Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1a690f0

Browse files
committed
Allow cross-origin requests between users' own apps
1 parent 125e9ef commit 1a690f0

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
@@ -406,9 +406,7 @@ func New(options *Options) *API {
406406
derpHandler, api.derpCloseFunc = tailnet.WithWebsocketSupport(api.DERPServer, derpHandler)
407407
cors := httpmw.Cors(options.DeploymentValues.Dangerous.AllowAllCors.Value())
408408
prometheusMW := httpmw.Prometheus(options.PrometheusRegistry)
409-
410409
r.Use(
411-
cors,
412410
httpmw.Recover(api.Logger),
413411
tracing.StatusWriterMiddleware,
414412
tracing.Middleware(api.TracerProvider),
@@ -419,9 +417,13 @@ func New(options *Options) *API {
419417
// SubdomainAppMW checks if the first subdomain is a valid app URL. If
420418
// it is, it will serve that application.
421419
//
422-
// Workspace apps do their own auth and must be BEFORE the auth
423-
// middleware.
420+
// Workspace apps do their own auth and CORS and must be BEFORE the auth
421+
// and CORS middleware.
422+
// REVIEW: Would it be worth creating httpmw.ExtractWorkspaceApp and using a
423+
// single CORS middleware?
424424
api.workspaceAppServer.HandleSubdomain(apiRateLimiter),
425+
// REVIEW: Is it OK that CORS come after the above middleware?
426+
cors,
425427
// Build-Version is helpful for debugging.
426428
func(next http.Handler) http.Handler {
427429
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)