chore: CORs option for yarn dev server (#7630)

* chore: Yarn dev servers require CORs headers for external proxies

Adds a flag to set CORs headers to `*` for yarn dev servers
This commit is contained in:
Steven Masley 2023-05-22 20:02:39 +02:00 committed by GitHub
parent 1f4f0efed6
commit 5d711fc95a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 80 additions and 18 deletions

3
coderd/apidoc/docs.go generated
View File

@ -7252,6 +7252,9 @@ const docTemplate = `{
"codersdk.DangerousConfig": {
"type": "object",
"properties": {
"allow_all_cors": {
"type": "boolean"
},
"allow_path_app_sharing": {
"type": "boolean"
},

View File

@ -6454,6 +6454,9 @@
"codersdk.DangerousConfig": {
"type": "object",
"properties": {
"allow_all_cors": {
"type": "boolean"
},
"allow_path_app_sharing": {
"type": "boolean"
},

View File

@ -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

27
coderd/httpmw/cors.go Normal file
View File

@ -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,
})
}

View File

@ -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))

View File

@ -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

View File

@ -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.",

View File

@ -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
},

View File

@ -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
},

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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