Severity: medium — spec violation on every same-origin / S2S response when the middleware is enabled with multiple allowed origins.
Surfaced by: titan Phase 2.6 (Wheels 4.0 upgrade), 2026-05-15.
Repro
set(middleware = [
new wheels.middleware.Cors(allowOrigins="https://portal.pai.com,https://portal.paiindustries.com")
]);
Request with no Origin header:
→ Response includes:
Access-Control-Allow-Origin: https://portal.pai.com,https://portal.paiindustries.com
The CORS spec requires Access-Control-Allow-Origin to be a single origin or * — never a comma-delimited list. This pollutes same-origin and server-to-server responses, may poison CDN/proxy caches that key on response headers, and confuses any client that inspects the header strictly.
Root cause
vendor/wheels/middleware/Cors.cfc:60-80:
local.allowOrigin = variables.allowOrigins; // raw comma-list as the default
if (Len(local.origin)) {
// ... only reassigns local.allowOrigin when an Origin header is present
}
cfheader(name="Access-Control-Allow-Origin", value=local.allowOrigin);
When Origin is absent, the default is never overwritten and the raw list goes out as the header value.
Suggested fix
Default local.allowOrigin = "" and only emit the header when there's a matched origin:
local.allowOrigin = "";
if (Len(local.origin) && originIsAllowed(local.origin)) {
local.allowOrigin = local.origin;
}
if (Len(local.allowOrigin)) {
cfheader(name="Access-Control-Allow-Origin", value=local.allowOrigin);
}
cc @bpamiri
Severity: medium — spec violation on every same-origin / S2S response when the middleware is enabled with multiple allowed origins.
Surfaced by: titan Phase 2.6 (Wheels 4.0 upgrade), 2026-05-15.
Repro
set(middleware = [ new wheels.middleware.Cors(allowOrigins="https://portal.pai.com,https://portal.paiindustries.com") ]);Request with no
Originheader:→ Response includes:
The CORS spec requires
Access-Control-Allow-Originto be a single origin or*— never a comma-delimited list. This pollutes same-origin and server-to-server responses, may poison CDN/proxy caches that key on response headers, and confuses any client that inspects the header strictly.Root cause
vendor/wheels/middleware/Cors.cfc:60-80:local.allowOrigin = variables.allowOrigins; // raw comma-list as the default if (Len(local.origin)) { // ... only reassigns local.allowOrigin when an Origin header is present } cfheader(name="Access-Control-Allow-Origin", value=local.allowOrigin);When
Originis absent, the default is never overwritten and the raw list goes out as the header value.Suggested fix
Default
local.allowOrigin = ""and only emit the header when there's a matched origin:local.allowOrigin = ""; if (Len(local.origin) && originIsAllowed(local.origin)) { local.allowOrigin = local.origin; } if (Len(local.allowOrigin)) { cfheader(name="Access-Control-Allow-Origin", value=local.allowOrigin); }cc @bpamiri