diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 7e5baefbf7..9062fae632 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -30304,7 +30304,7 @@ components: items: type: string maxLength: 256 - description: "Origins allowed to call this route cross-origin, matched against the request's Origin header (ignoring case) and echoed back on a match. When set, the list governs both the preflight and the response, overriding any Access-Control-Allow-Origin the runnable returns via wm_headers. Use ['*'] to opt out of any restriction, including the http_route_default_allowed_origins instance setting. An empty list is not a configuration and resolves exactly as null does. When null, the instance setting applies, or Access-Control-Allow-Origin: * if it is unset." + description: "Origins allowed to call this route cross-origin, matched against the request's Origin header (ignoring case) and echoed back on a match. When set, the list governs both the preflight and the response, overriding any Access-Control-Allow-Origin the runnable returns via wm_headers. Use ['*'] to opt out of any restriction, including the http_route_default_allowed_origins instance setting. An empty list is not a configuration and resolves exactly as null does. When null, the instance setting applies, or Access-Control-Allow-Origin: * if it is unset. Ignored on a route serving static assets: those hand out public files, so restricting which browsers may read them protects nothing while breaking cross-origin webfonts and fetches." error_handler_path: type: string description: Path to a script to run when the triggered job fails. A bare @@ -30403,7 +30403,7 @@ components: items: type: string maxLength: 256 - description: "Origins allowed to call this route cross-origin, matched against the request's Origin header (ignoring case) and echoed back on a match. When set, the list governs both the preflight and the response, overriding any Access-Control-Allow-Origin the runnable returns via wm_headers. Use ['*'] to opt out of any restriction, including the http_route_default_allowed_origins instance setting. An empty list is not a configuration and resolves exactly as null does. When null, the instance setting applies, or Access-Control-Allow-Origin: * if it is unset." + description: "Origins allowed to call this route cross-origin, matched against the request's Origin header (ignoring case) and echoed back on a match. When set, the list governs both the preflight and the response, overriding any Access-Control-Allow-Origin the runnable returns via wm_headers. Use ['*'] to opt out of any restriction, including the http_route_default_allowed_origins instance setting. An empty list is not a configuration and resolves exactly as null does. When null, the instance setting applies, or Access-Control-Allow-Origin: * if it is unset. Ignored on a route serving static assets: those hand out public files, so restricting which browsers may read them protects nothing while breaking cross-origin webfonts and fetches." error_handler_path: type: string description: Path to a script to run when the triggered job fails. A bare @@ -30509,7 +30509,7 @@ components: items: type: string maxLength: 256 - description: "Origins allowed to call this route cross-origin, matched against the request's Origin header (ignoring case) and echoed back on a match. When set, the list governs both the preflight and the response, overriding any Access-Control-Allow-Origin the runnable returns via wm_headers. Use ['*'] to opt out of any restriction, including the http_route_default_allowed_origins instance setting. An empty list is not a configuration and resolves exactly as null does. When null, the instance setting applies, or Access-Control-Allow-Origin: * if it is unset." + description: "Origins allowed to call this route cross-origin, matched against the request's Origin header (ignoring case) and echoed back on a match. When set, the list governs both the preflight and the response, overriding any Access-Control-Allow-Origin the runnable returns via wm_headers. Use ['*'] to opt out of any restriction, including the http_route_default_allowed_origins instance setting. An empty list is not a configuration and resolves exactly as null does. When null, the instance setting applies, or Access-Control-Allow-Origin: * if it is unset. Ignored on a route serving static assets: those hand out public files, so restricting which browsers may read them protects nothing while breaking cross-origin webfonts and fetches." error_handler_path: type: string description: Path to a script to run when the triggered job fails. A bare diff --git a/backend/windmill-api/src/triggers/http/handler.rs b/backend/windmill-api/src/triggers/http/handler.rs index 9b53814bdf..d50abdeee1 100644 --- a/backend/windmill-api/src/triggers/http/handler.rs +++ b/backend/windmill-api/src/triggers/http/handler.rs @@ -102,6 +102,12 @@ enum CorsDecision { Unavailable, } +/// A route serving static assets is never subject to an allowlist, its own +/// included. It hands out public files that any non-browser client can already +/// fetch, so restricting which browsers may read them protects nothing, while +/// breaking the cross-origin uses that do consult CORS: a webfont, a +/// `crossorigin` asset, a `fetch`. +/// /// The CORS verdict for a request, published by whoever resolved its trigger. /// /// The middleware stamps headers after the handler returns, but only the @@ -117,16 +123,20 @@ impl ResolvedCorsPolicy { /// Record what the trigger being served allows. Called once, where the /// route is resolved, so the answer cannot drift from the response. fn publish(&self, trigger: &TriggerRoute, method: Option, headers: &HeaderMap) { - let instance_default = HTTP_ROUTE_DEFAULT_ALLOWED_ORIGINS.load(); - let decision = match effective_allowed_origins( - trigger.allowed_origins.as_deref(), - instance_default.as_slice(), - ) { - None => CorsDecision::Unrestricted, - Some(allowed_origins) => CorsDecision::Restricted { - route_method: method, - allow_origin: match_origin(allowed_origins, headers.get(http::header::ORIGIN)), - }, + let decision = if trigger.static_asset_config.is_some() { + CorsDecision::Unrestricted + } else { + let instance_default = HTTP_ROUTE_DEFAULT_ALLOWED_ORIGINS.load(); + match effective_allowed_origins( + trigger.allowed_origins.as_deref(), + instance_default.as_slice(), + ) { + None => CorsDecision::Unrestricted, + Some(allowed_origins) => CorsDecision::Restricted { + route_method: method, + allow_origin: match_origin(allowed_origins, headers.get(http::header::ORIGIN)), + }, + } }; let _ = self.0.set(decision); } @@ -170,6 +180,12 @@ async fn resolve_cors_decision( }; let route = router.at(requested_path).ok(); + if route + .as_ref() + .is_some_and(|trigger| trigger.value.static_asset_config.is_some()) + { + return CorsDecision::Unrestricted; + } let route_allowed_origins = route .as_ref() .and_then(|trigger| trigger.value.allowed_origins.as_deref()); diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index a95fc8df1b..7a690e80be 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -8395,7 +8395,10 @@ properties: the runnable returns via wm_headers. Use [''*''] to opt out of any restriction, including the http_route_default_allowed_origins instance setting. An empty list is not a configuration and resolves exactly as null does. When null, the - instance setting applies, or Access-Control-Allow-Origin: * if it is unset.' + instance setting applies, or Access-Control-Allow-Origin: * if it is unset. + Ignored on a route serving static assets: those hand out public files, so restricting + which browsers may read them protects nothing while breaking cross-origin webfonts + and fetches.' error_handler_path: type: string description: Path to a script to run when the triggered job fails. A bare path, diff --git a/frontend/src/lib/components/copilot/chat/workspaceToolsZod.gen.ts b/frontend/src/lib/components/copilot/chat/workspaceToolsZod.gen.ts index c6db05611e..2153e9304b 100644 --- a/frontend/src/lib/components/copilot/chat/workspaceToolsZod.gen.ts +++ b/frontend/src/lib/components/copilot/chat/workspaceToolsZod.gen.ts @@ -69,7 +69,7 @@ export const httpTriggerRequestSchema = z.object({ "wrap_body": z.boolean().describe("If true, wraps the request body in a 'body' parameter").optional(), "mode": z.enum(["enabled", "disabled", "suspended"]).describe("job trigger mode").optional(), "raw_string": z.boolean().describe("If true, passes the request body as a raw string instead of parsing as JSON").optional(), - "allowed_origins": z.array(z.string()).describe("Origins allowed to call this route cross-origin, matched against the request's Origin header (ignoring case) and echoed back on a match. When set, the list governs both the preflight and the response, overriding any Access-Control-Allow-Origin the runnable returns via wm_headers. Use ['*'] to opt out of any restriction, including the http_route_default_allowed_origins instance setting. An empty list is not a configuration and resolves exactly as null does. When null, the instance setting applies, or Access-Control-Allow-Origin: * if it is unset.").nullable().optional(), + "allowed_origins": z.array(z.string()).describe("Origins allowed to call this route cross-origin, matched against the request's Origin header (ignoring case) and echoed back on a match. When set, the list governs both the preflight and the response, overriding any Access-Control-Allow-Origin the runnable returns via wm_headers. Use ['*'] to opt out of any restriction, including the http_route_default_allowed_origins instance setting. An empty list is not a configuration and resolves exactly as null does. When null, the instance setting applies, or Access-Control-Allow-Origin: * if it is unset. Ignored on a route serving static assets: those hand out public files, so restricting which browsers may read them protects nothing while breaking cross-origin webfonts and fetches.").nullable().optional(), "error_handler_path": z.string().describe("Path to a script to run when the triggered job fails. A bare path, without the script/ or flow/ prefix a schedule error handler takes; it cannot be a flow.").optional(), "error_handler_args": z.record(z.string(), z.any()).describe("Arguments to pass to the error handler").optional(), "retry": z.object({ diff --git a/system_prompts/auto-generated/schemas/http_trigger.schema.yaml b/system_prompts/auto-generated/schemas/http_trigger.schema.yaml index c5e634da07..83f1b8ac47 100644 --- a/system_prompts/auto-generated/schemas/http_trigger.schema.yaml +++ b/system_prompts/auto-generated/schemas/http_trigger.schema.yaml @@ -105,7 +105,10 @@ properties: the runnable returns via wm_headers. Use [''*''] to opt out of any restriction, including the http_route_default_allowed_origins instance setting. An empty list is not a configuration and resolves exactly as null does. When null, the - instance setting applies, or Access-Control-Allow-Origin: * if it is unset.' + instance setting applies, or Access-Control-Allow-Origin: * if it is unset. + Ignored on a route serving static assets: those hand out public files, so restricting + which browsers may read them protects nothing while breaking cross-origin webfonts + and fetches.' error_handler_path: type: string description: Path to a script to run when the triggered job fails. A bare path,