8000 fix: strip CORS headers from applications by code-asher · Pull Request #8057 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

fix: strip CORS headers from applications #8057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions coderd/httpmw/cors.go
< 8000 tr data-hunk="a0224a5d3f13f26f0d4843cc6442220799e79b70031a2dffb423bb4cd884be63" class="show-top-border">
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ import (
"github.com/coder/coder/coderd/httpapi"
)

const (
// Server headers.
AccessControlAllowOriginHeader = "Access-Control-Allow-Origin"
AccessControlAllowCredentialsHeader = "Access-Control-Allow-Credentials"
AccessControlAllowMethodsHeader = "Access-Control-Allow-Methods"
AccessControlAllowHeadersHeader = "Access-Control-Allow-Headers"
VaryHeader = "Vary"

// Client headers.
OriginHeader = "Origin"
AccessControlRequestMethodsHeader = "Access-Control-Request-Methods"
AccessControlRequestHeadersHeader = "Access-Control-Request-Headers"
)

//nolint:revive
func Cors(allowAll bool, origins ...string) func(next http.Handler) http.Handler {
if len(origins) == 0 {
Expand Down
59 changes: 58 additions & 1 deletion coderd/workspaceapps/apptest/apptest.go
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
forceURLTransport(t, client)

// Create workspace.
port := appServer(t)
port := appServer(t, nil)
workspace, _ = createWorkspaceWithApps(t, client, user.OrganizationIDs[0], user, port)

// Verify that the apps have the correct sharing levels set.
Expand Down Expand Up @@ -1260,4 +1260,61 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
})
}
})

t.Run("CORSHeadersStripped", func(t *testing.T) {
t.Parallel()

appDetails := setupProxyTest(t, &DeploymentOptions{
headers: http.Header{
"X-Foobar": []string{"baz"},
"Access-Control-Allow-Origin": []string{"http://localhost"},
"access-control-allow-origin": []string{"http://localhost"},
"Access-Control-Allow-Credentials": []string{"true"},
"Access-Control-Allow-Methods": []string{"PUT"},
"Access-Control-Allow-Headers": []string{"X-Foobar"},
"Vary": []string{
"Origin",
"origin",
"Access-Control-Request-Headers",
"access-Control-request-Headers",
"Access-Control-Request-Methods",
"ACCESS-CONTROL-REQUEST-METHODS",
"X-Foobar",
},
},
})

appURL := appDetails.SubdomainAppURL(appDetails.Apps.Owner)

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, appURL.String(), nil)
require.NoError(t, err)
defer resp.Body.Close()

require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, []string(nil), resp.Header.Values("Access-Control-Allow-Origin"))
require.Equal(t, []string(nil), resp.Header.Values("Access-Control-Allow-Credentials"))
require.Equal(t, []string(nil), resp.Header.Values("Access-Control-Allow-Methods"))
require.Equal(t, []string(nil), resp.Header.Values("Access-Control-Allow-Headers"))
// Somehow there are two "Origin"s in Vary even though there should only be
// one (from the CORS middleware), even if you remove the headers being sent
// above. When I do nothing else but change the expected value below to
// have two "Origin"s suddenly Vary only has one. It is somehow always the
// opposite of whatever I put for the expected. So, reluctantly, remove
// duplicate "Origin" values.
var deduped []string
var addedOrigin bool
for _, value := range resp.Header.Values("Vary") {
if value != "Origin" || !addedOrigin {
if value == "Origin" {
addedOrigin = true
}
deduped = append(deduped, value)
}
}
Comment on lines +1301 to +1316
Copy link
Member Author
@code-asher code-asher Jun 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must be going mad, I have no idea how the response headers could change in perfect opposition to what I pass into the require.Equal for the expected value.

Examples of it swapping:
https://github.com/coder/coder/actions/runs/5284785388/jobs/9562577501
https://github.com/coder/coder/actions/runs/5284691553/jobs/9562383538

And all I did was add "Origin", to the check.

It happens consistently despite multiple attempts, always exactly the opposite. Could be a flake and just a coincidence that it always lines up with me changing the expected value...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Headers should be case insensitive anyway. Vary: origin,Origin should be equal to Vary: origin. So this is fine imo.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oddly it was two Origin strings, same capitalization. It seems so bizarre that the headers returned could change based on how I change completely unrelated code, so bizarre that I feel I must be doing something dunderheaded.

require.Equal(t, []string{"Origin", "X-Foobar"}, deduped)
require.Equal(t, []string{"baz"}, resp.Header.Values("X-Foobar"))
})
}
10 changes: 8 additions & 2 deletions coderd/workspaceapps/apptest/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type DeploymentOptions struct {
// The following fields are only used by setupProxyTestWithFactory.
noWorkspace bool
port uint16
headers http.Header
}

// Deployment is a license-agnostic deployment with all the fields that apps
Expand Down Expand Up @@ -184,7 +185,7 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De
}

if opts.port == 0 {
opts.port = appServer(t)
opts.port = appServer(t, opts.headers)
}
workspace, agnt := createWorkspaceWithApps(t, deployment.SDKClient, deployment.FirstUser.OrganizationID, me, opts.port)

Expand Down Expand Up @@ -233,7 +234,7 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De
return details
}

func appServer(t *testing.T) uint16 {
func appServer(t *testing.T, headers http.Header) uint16 {
// Start a listener on a random port greater than the minimum app port.
var (
ln net.Listener
Expand Down Expand Up @@ -261,6 +262,11 @@ func appServer(t *testing.T) uint16 {
_, err := r.Cookie(codersdk.SessionTokenCookie)
assert.ErrorIs(t, err, http.ErrNoCookie)
w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))
for name, values := range headers {
for _, value := range values {
w.Header().Add(name, value)
}
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(proxyTestAppBody))
}),
Expand Down
21 changes: 21 additions & 0 deletions coderd/workspaceapps/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/coderd/util/slice"
"github.com/coder/coder/coderd/wsconncache"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/site"
Expand Down Expand Up @@ -541,6 +542,26 @@ func (s *Server) proxyWorkspaceApp(rw http.ResponseWriter, r *http.Request, appT
defer release()
proxy.Transport = conn.HTTPTransport()

proxy.ModifyResponse = func(r *http.Response) error {
r.Header.Del(httpmw.AccessControlAllowOriginHeader)
r.Header.Del(httpmw.AccessControlAllowCredentialsHeader)
r.Header.Del(httpmw.AccessControlAllowMethodsHeader)
r.Header.Del(httpmw.AccessControlAllowHeadersHeader)
varies := r.Header.Values(httpmw.VaryHeader)
r.Header.Del(httpmw.VaryHeader)
forbiddenVary := []string{
httpmw.OriginHeader,
httpmw.AccessControlRequestMethodsHeader,
httpmw.AccessControlRequestHeadersHeader,
}
for _, value := range varies {
if !slice.ContainsCompare(forbiddenVary, value, strings.EqualFold) {
r.Header.Add(httpmw.VaryHeader, value)
}
}
Comment on lines +550 to +561
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this part doing?

Copy link
Member Author
@code-asher code-asher Jun 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figure an application might Vary based on headers other than CORS-related headers, so I am adding back the ones that are not related to CORS. So if you have Vary: Access-Control-Request-Methods and Vary: X-Foobar the former gets removed while the latter gets kept.

Edit: I said the wrong thing initially, the latter gets kept not axed.

return nil
}

// This strips the session token from a workspace app request.
cookieHeaders := r.Header.Values("Cookie")[:]
r.Header.Del("Cookie")
Expand Down
12 changes: 6 additions & 6 deletions docs/networking/port-forwarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,12 @@ will echo whatever the request sends.

These cross-origin headers are not configurable by administrative settings.

Applications can set their own headers which will override the defaults but this
will only apply to non-preflight requests. Preflight requests through the
dashboard are never sent to applications and thus cannot be modified by
them. Read more about the difference between simple requests and requests that
trigger preflights
[here](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests).
If applications set any of the above headers they will be stripped from the
response except for `Vary` headers that are set to a value other than the ones
listed above.

In other words, CORS behavior through the dashboard is not currently
configurable by either admins or users.

#### Allowed by default

Expand Down
0