diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 9bf6444938..871ce342cd 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -7252,6 +7252,9 @@ const docTemplate = `{ "codersdk.DangerousConfig": { "type": "object", "properties": { + "allow_all_cors": { + "type": "boolean" + }, "allow_path_app_sharing": { "type": "boolean" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index e82bf43527..208f44d940 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -6454,6 +6454,9 @@ "codersdk.DangerousConfig": { "type": "object", "properties": { + "allow_all_cors": { + "type": "boolean" + }, "allow_path_app_sharing": { "type": "boolean" }, diff --git a/coderd/coderd.go b/coderd/coderd.go index 8f71dfee06..2e124fc1fd 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -393,8 +393,10 @@ func New(options *Options) *API { derpHandler := derphttp.Handler(api.DERPServer) derpHandler, api.derpCloseFunc = tailnet.WithWebsocketSupport(api.DERPServer, derpHandler) + cors := httpmw.Cors(options.DeploymentValues.Dangerous.AllowAllCors.Value()) r.Use( + cors, httpmw.Recover(api.Logger), tracing.StatusWriterMiddleware, tracing.Middleware(api.TracerProvider), @@ -799,6 +801,10 @@ func New(options *Options) *API { // Add CSP headers to all static assets and pages. CSP headers only affect // browsers, so these don't make sense on api routes. cspMW := httpmw.CSPHeaders(func() []string { + if api.DeploymentValues.Dangerous.AllowAllCors { + // In this mode, allow all external requests + return []string{"*"} + } if f := api.WorkspaceProxyHostsFn.Load(); f != nil { return (*f)() } @@ -813,7 +819,7 @@ func New(options *Options) *API { // This is the only route we add before all the middleware. // We want to time the latency of the request, so any middleware will // interfere with that timing. - rootRouter.Get("/latency-check", LatencyCheck(api.AccessURL)) + rootRouter.Get("/latency-check", cors(LatencyCheck(options.DeploymentValues.Dangerous.AllowAllCors.Value(), api.AccessURL)).ServeHTTP) rootRouter.Mount("/", r) api.RootHandler = rootRouter diff --git a/coderd/httpmw/cors.go b/coderd/httpmw/cors.go new file mode 100644 index 0000000000..7c7e89fa53 --- /dev/null +++ b/coderd/httpmw/cors.go @@ -0,0 +1,27 @@ +package httpmw + +import ( + "net/http" + + "github.com/go-chi/cors" +) + +//nolint:revive +func Cors(allowAll bool, origins ...string) func(next http.Handler) http.Handler { + if len(origins) == 0 { + // The default behavior is '*', so putting the empty string defaults to + // the secure behavior of blocking CORs requests. + origins = []string{""} + } + if allowAll { + origins = []string{"*"} + } + return cors.Handler(cors.Options{ + AllowedOrigins: origins, + // We only need GET for latency requests + AllowedMethods: []string{http.MethodOptions, http.MethodGet}, + AllowedHeaders: []string{"Accept", "Content-Type", "X-LATENCY-CHECK", "X-CSRF-TOKEN"}, + // Do not send any cookies + AllowCredentials: false, + }) +} diff --git a/coderd/httpmw/csp.go b/coderd/httpmw/csp.go index 0721e97963..fde5c62d8b 100644 --- a/coderd/httpmw/csp.go +++ b/coderd/httpmw/csp.go @@ -103,6 +103,11 @@ func CSPHeaders(websocketHosts func() []string) func(next http.Handler) http.Han extraConnect := websocketHosts() if len(extraConnect) > 0 { for _, extraHost := range extraConnect { + if extraHost == "*" { + // '*' means all + cspSrcs.Append(cspDirectiveConnectSrc, "*") + continue + } cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", extraHost)) // We also require this to make http/https requests to the workspace proxy for latency checking. cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("https://%[1]s http://%[1]s", extraHost)) diff --git a/coderd/latencycheck.go b/coderd/latencycheck.go index 339db9bf9c..84d12841a7 100644 --- a/coderd/latencycheck.go +++ b/coderd/latencycheck.go @@ -6,7 +6,12 @@ import ( "strings" ) -func LatencyCheck(allowedOrigins ...*url.URL) http.HandlerFunc { +// LatencyCheck is an endpoint for the web ui to measure latency with. +// allowAll allows any Origin to get timing information. The allowAll should +// only be set in dev modes. +// +//nolint:revive +func LatencyCheck(allowAll bool, allowedOrigins ...*url.URL) http.HandlerFunc { allowed := make([]string, 0, len(allowedOrigins)) for _, origin := range allowedOrigins { // Allow the origin without a path @@ -14,6 +19,9 @@ func LatencyCheck(allowedOrigins ...*url.URL) http.HandlerFunc { tmp.Path = "" allowed = append(allowed, strings.TrimSuffix(origin.String(), "/")) } + if allowAll { + allowed = append(allowed, "*") + } origins := strings.Join(allowed, ",") return func(rw http.ResponseWriter, r *http.Request) { // Allowing timing information to be shared. This allows the browser diff --git a/codersdk/deployment.go b/codersdk/deployment.go index a1d5531052..84c295aefe 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -330,6 +330,7 @@ type LoggingConfig struct { type DangerousConfig struct { AllowPathAppSharing clibase.Bool `json:"allow_path_app_sharing" typescript:",notnull"` AllowPathAppSiteOwnerAccess clibase.Bool `json:"allow_path_app_site_owner_access" typescript:",notnull"` + AllowAllCors clibase.Bool `json:"allow_all_cors" typescript:",notnull"` } const ( @@ -1167,6 +1168,16 @@ when required by your organization's security policy.`, Annotations: clibase.Annotations{}.Mark(annotationExternalProxies, "true"), }, // ☢️ Dangerous settings + { + Name: "DANGEROUS: Allow all CORs requests", + 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.", + Flag: "dangerous-allow-cors-requests", + Env: "CODER_DANGEROUS_ALLOW_CORS_REQUESTS", + Hidden: true, // Hidden, should only be used by yarn dev server + Value: &c.Dangerous.AllowAllCors, + Group: &deploymentGroupDangerous, + Annotations: clibase.Annotations{}.Mark(annotationExternalProxies, "true"), + }, { Name: "DANGEROUS: Allow Path App Sharing", Description: "Allow workspace apps that are not served from subdomains to be shared. Path-based app sharing is DISABLED by default for security purposes. Path-based apps can make requests to the Coder API and pose a security risk when the workspace serves malicious JavaScript. Path-based apps can be disabled entirely with --disable-path-apps for further security.", diff --git a/docs/api/general.md b/docs/api/general.md index 7c8e62b894..b1b713a66b 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -161,6 +161,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "sshconfigOptions": ["string"] }, "dangerous": { + "allow_all_cors": true, "allow_path_app_sharing": true, "allow_path_app_site_owner_access": true }, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 6d527ee8d2..f48f814755 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1800,6 +1800,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a ```json { + "allow_all_cors": true, "allow_path_app_sharing": true, "allow_path_app_site_owner_access": true } @@ -1809,6 +1810,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a | Name | Type | Required | Restrictions | Description | | ---------------------------------- | ------- | -------- | ------------ | ----------- | +| `allow_all_cors` | boolean | false | | | | `allow_path_app_sharing` | boolean | false | | | | `allow_path_app_site_owner_access` | boolean | false | | | @@ -1857,6 +1859,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a "sshconfigOptions": ["string"] }, "dangerous": { + "allow_all_cors": true, "allow_path_app_sharing": true, "allow_path_app_site_owner_access": true }, @@ -2201,6 +2204,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a "sshconfigOptions": ["string"] }, "dangerous": { + "allow_all_cors": true, "allow_path_app_sharing": true, "allow_path_app_site_owner_access": true }, diff --git a/enterprise/cli/proxyserver.go b/enterprise/cli/proxyserver.go index eabc1c7659..d4a1de57c6 100644 --- a/enterprise/cli/proxyserver.go +++ b/enterprise/cli/proxyserver.go @@ -245,6 +245,7 @@ func (*RootCmd) proxyServer() *clibase.Cmd { SecureAuthCookie: cfg.SecureAuthCookie.Value(), DisablePathApps: cfg.DisablePathApps.Value(), ProxySessionToken: proxySessionToken.Value(), + AllowAllCors: cfg.Dangerous.AllowAllCors.Value(), }) if err != nil { return xerrors.Errorf("create workspace proxy: %w", err) diff --git a/enterprise/wsproxy/wsproxy.go b/enterprise/wsproxy/wsproxy.go index 7548f7c685..2c91ba1555 100644 --- a/enterprise/wsproxy/wsproxy.go +++ b/enterprise/wsproxy/wsproxy.go @@ -11,7 +11,6 @@ import ( "time" "github.com/go-chi/chi/v5" - "github.com/go-chi/cors" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/trace" @@ -61,6 +60,10 @@ type Options struct { DisablePathApps bool ProxySessionToken string + // AllowAllCors will set all CORs headers to '*'. + // By default, CORs is set to accept external requests + // from the dashboardURL. This should only be used in development. + AllowAllCors bool } func (o *Options) Validate() error { @@ -189,18 +192,7 @@ func New(ctx context.Context, opts *Options) (*Server, error) { // The primary coderd dashboard needs to make some GET requests to // the workspace proxies to check latency. - corsMW := cors.Handler(cors.Options{ - AllowedOrigins: []string{ - // Allow the dashboard to make requests to the proxy for latency - // checks. - opts.DashboardURL.String(), - }, - // Only allow GET requests for latency checks. - AllowedMethods: []string{http.MethodOptions, http.MethodGet}, - AllowedHeaders: []string{"Accept", "Content-Type", "X-LATENCY-CHECK", "X-CSRF-TOKEN"}, - // Do not send any cookies - AllowCredentials: false, - }) + corsMW := httpmw.Cors(opts.AllowAllCors, opts.DashboardURL.String()) // Routes apiRateLimiter := httpmw.RateLimit(opts.APIRateLimit, time.Minute) @@ -266,7 +258,7 @@ func New(ctx context.Context, opts *Options) (*Server, error) { // See coderd/coderd.go for why we need this. rootRouter := chi.NewRouter() // Make sure to add the cors middleware to the latency check route. - rootRouter.Get("/latency-check", corsMW(coderd.LatencyCheck(s.DashboardURL, s.AppServer.AccessURL)).ServeHTTP) + rootRouter.Get("/latency-check", corsMW(coderd.LatencyCheck(opts.AllowAllCors, s.DashboardURL, s.AppServer.AccessURL)).ServeHTTP) rootRouter.Mount("/", r) s.Handler = rootRouter diff --git a/scripts/develop.sh b/scripts/develop.sh index 6179676c26..f27235e4da 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -131,7 +131,7 @@ fatal() { trap 'fatal "Script encountered an error"' ERR cdroot - start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" --experiments "*" "$@" + start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" --dangerous-allow-cors-requests=true --experiments "*" "$@" echo '== Waiting for Coder to become ready' # Start the timeout in the background so interrupting this script @@ -185,7 +185,7 @@ fatal() { # Create the proxy proxy_session_token=$("${CODER_DEV_SHIM}" wsproxy create --name=local-proxy --display-name="Local Proxy" --icon="/emojis/1f4bb.png" --only-token) # Start the proxy - start_cmd PROXY "" "${CODER_DEV_SHIM}" wsproxy server --http-address=localhost:3010 --proxy-session-token="${proxy_session_token}" --primary-access-url=http://localhost:3000 + start_cmd PROXY "" "${CODER_DEV_SHIM}" wsproxy server --dangerous-allow-cors-requests=true --http-address=localhost:3010 --proxy-session-token="${proxy_session_token}" --primary-access-url=http://localhost:3000 ) || echo "Failed to create workspace proxy. No workspace proxy created." fi diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index fbd8b6da08..7d6bb09791 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -313,6 +313,7 @@ export interface DERPServerConfig { export interface DangerousConfig { readonly allow_path_app_sharing: boolean readonly allow_path_app_site_owner_access: boolean + readonly allow_all_cors: boolean } // From codersdk/deployment.go