Skip to content

Commit 28ec76b

Browse files
committed
Allow cross-origin requests between users' own apps
1 parent 5eb41e8 commit 28ec76b

File tree

3 files changed

+77
-31
lines changed

3 files changed

+77
-31
lines changed

coderd/coderd.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,9 +394,7 @@ func New(options *Options) *API {
394394
derpHandler := derphttp.Handler(api.DERPServer)
395395
derpHandler, api.derpCloseFunc = tailnet.WithWebsocketSupport(api.DERPServer, derpHandler)
396396
cors := httpmw.Cors(options.DeploymentValues.Dangerous.AllowAllCors.Value())
397-
398397
r.Use(
399-
cors,
400398
httpmw.Recover(api.Logger),
401399
tracing.StatusWriterMiddleware,
402400
tracing.Middleware(api.TracerProvider),
@@ -407,9 +405,13 @@ func New(options *Options) *API {
407405
// SubdomainAppMW checks if the first subdomain is a valid app URL. If
408406
// it is, it will serve that application.
409407
//
410-
// Workspace apps do their own auth and must be BEFORE the auth
411-
// middleware.
408+
// Workspace apps do their own auth and CORS and must be BEFORE the auth
409+
// and CORS middleware.
410+
// REVIEW: Would it be worth creating httpmw.ExtractWorkspaceApp and using a
411+
// single CORS middleware?
412412
api.workspaceAppServer.HandleSubdomain(apiRateLimiter),
413+
// REVIEW: Is it OK that CORS come after the above middleware?
414+
cors,
413415
// Build-Version is helpful for debugging.
414416
func(next http.Handler) http.Handler {
415417
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

coderd/workspaceapps/proxy.go

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"sync"
1414

1515
"github.com/go-chi/chi/v5"
16+
"github.com/go-chi/cors"
1617
"github.com/google/uuid"
1718
"go.opentelemetry.io/otel/trace"
1819
"nhooyr.io/websocket"
@@ -361,41 +362,84 @@ func (s *Server) HandleSubdomain(middlewares ...func(http.Handler) http.Handler)
361362
return
362363
}
363364

364-
if !s.handleAPIKeySmuggling(rw, r, AccessMethodSubdomain) {
365-
return
366-
}
367-
368-
token, ok := ResolveRequest(rw, r, ResolveRequestOptions{
369-
Logger: s.Logger,
370-
SignedTokenProvider: s.SignedTokenProvider,
371-
DashboardURL: s.DashboardURL,
372-
PathAppBaseURL: s.AccessURL,
373-
AppHostname: s.Hostname,
374-
AppRequest: Request{
375-
AccessMethod: AccessMethodSubdomain,
376-
BasePath: "/",
377-
UsernameOrID: app.Username,
378-
WorkspaceNameOrID: app.WorkspaceName,
379-
AgentNameOrID: app.AgentName,
380-
AppSlugOrPort: app.AppSlugOrPort,
381-
},
382-
AppPath: r.URL.Path,
383-
AppQuery: r.URL.RawQuery,
384-
})
385-
if !ok {
386-
return
365+
// REVIEW: Like mentioned in coderd.go maybe we should extract the app
366+
// using middleware that way we can do this in a single top-level CORS
367+
// handler? Or just do the URL parsing twice.
368+
var corsmw func(next http.Handler) http.Handler
369+
origin := r.Header.Get("Origin")
370+
if originApp, ok := s.parseOrigin(origin); ok && originApp.Username == app.Username {
371+
corsmw = cors.Handler(cors.Options{
372+
AllowedOrigins: []string{origin},
373+
AllowedMethods: []string{
374+
http.MethodHead,
375+
http.MethodGet,
376+
http.MethodPost,
377+
http.MethodPut,
378+
http.MethodPatch,
379+
http.MethodDelete,
380+
},
381+
AllowedHeaders: []string{"*"},
382+
AllowCredentials: true,
383+
})
384+
} else {
385+
corsmw = cors.Handler(cors.Options{
386+
AllowedOrigins: []string{""}, // The middleware defaults to *.
387+
AllowedMethods: []string{},
388+
AllowedHeaders: []string{},
389+
AllowCredentials: false,
390+
})
387391
}
388392

389-
// Use the passed in app middlewares before passing to the proxy
390-
// app.
391-
mws := chi.Middlewares(middlewares)
393+
// Use the passed in app middlewares before checking authentication and
394+
// passing to the proxy app.
395+
mws := chi.Middlewares(append(middlewares, corsmw))
392396
mws.Handler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
397+
if !s.handleAPIKeySmuggling(rw, r, AccessMethodSubdomain) {
398+
return
399+
}
400+
401+
token, ok := ResolveRequest(rw, r, ResolveRequestOptions{
402+
Logger: s.Logger,
403+
SignedTokenProvider: s.SignedTokenProvider,
404+
DashboardURL: s.DashboardURL,
405+
PathAppBaseURL: s.AccessURL,
406+
AppHostname: s.Hostname,
407+
AppRequest: Request{
408+
AccessMethod: AccessMethodSubdomain,
409+
BasePath: "/",
410+
UsernameOrID: app.Username,
411+
WorkspaceNameOrID: app.WorkspaceName,
412+
AgentNameOrID: app.AgentName,
413+
AppSlugOrPort: app.AppSlugOrPort,
414+
},
415+
AppPath: r.URL.Path,
416+
AppQuery: r.URL.RawQuery,
417+
})
418+
if !ok {
419+
return
420+
}
393421
s.proxyWorkspaceApp(rw, r, *token, r.URL.Path)
394422
})).ServeHTTP(rw, r.WithContext(ctx))
395423
})
396424
}
397425
}
398426

427+
func (s *Server) parseOrigin(rawOrigin string) (httpapi.ApplicationURL, bool) {
428+
origin, err := url.Parse(rawOrigin)
429+
if rawOrigin == "" || origin.Host == "" || err != nil {
430+
return httpapi.ApplicationURL{}, false
431+
}
432+
subdomain, ok := httpapi.ExecuteHostnamePattern(s.HostnameRegex, origin.Host)
433+
if !ok {
434+
return httpapi.ApplicationURL{}, false
435+
}
436+
app, err := httpapi.ParseSubdomainAppURL(subdomain)
437+
if err != nil {
438+
return httpapi.ApplicationURL{}, false
439+
}
440+
return app, true
441+
}
442+
399443
// parseHostname will return if a given request is attempting to access a
400444
// workspace app via a subdomain. If it is, the hostname of the request is parsed
401445
// into an httpapi.ApplicationURL and true is returned. If the request is not

codersdk/deployment.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1170,7 +1170,7 @@ when required by your organization's security policy.`,
11701170
// ☢️ Dangerous settings
11711171
{
11721172
Name: "DANGEROUS: Allow all CORs requests",
1173-
Description: "For security reasons, CORs requests are blocked. If external requests are required, setting this to true will set all cors headers as '*'. This should never be used in production.",
1173+
Description: "For security reasons, CORs requests are blocked except between workspace apps owned by the same user. If external requests are required, setting this to true will set all cors headers as '*'. This should never be used in production.",
11741174
Flag: "dangerous-allow-cors-requests",
11751175
Env: "CODER_DANGEROUS_ALLOW_CORS_REQUESTS",
11761176
Hidden: true, // Hidden, should only be used by yarn dev server

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy