Skip to content

Commit 926bad5

Browse files
penalosaemily-shendevin-ai-integration[bot]
authored
[wrangler] Validate header rules reject multiple wildcards (#12276)
Co-authored-by: emily-shen <eshen@cloudflare.com> Co-authored-by: emily-shen <69125074+emily-shen@users.noreply.github.com> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 194d75e commit 926bad5

4 files changed

Lines changed: 140 additions & 16 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@cloudflare/workers-shared": patch
3+
---
4+
5+
Warn when `_headers` rules contain multiple wildcards or wildcard combined with `:splat`
6+
7+
Rules containing multiple wildcards (e.g. `https://*.workers.dev/*`) or combining a wildcard with a `:splat` placeholder (e.g. `https://*.pages.dev/:splat`) are now rejected during parsing. Previously this would fail silently during dev.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
/
2-
X-Header: Custom-Value
2+
X-Header: Custom-Value

packages/workers-shared/utils/configuration/parseHeaders.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
HEADER_SEPARATOR,
33
MAX_HEADER_RULES,
44
MAX_LINE_LENGTH,
5+
SPLAT_REGEX,
56
UNSET_OPERATOR,
67
} from "./constants";
78
import { validateUrl } from "./validateURL";
@@ -24,6 +25,10 @@ export function parseHeaders(
2425
const invalid: InvalidHeadersRule[] = [];
2526

2627
let rule: (HeadersRule & { line: string }) | undefined = undefined;
28+
// When a path line is rejected (invalid URL, multiple wildcards, etc.),
29+
// we silently skip subsequent header lines until the next path line
30+
// rather than emitting confusing "Path should come before header" errors.
31+
let skipUntilNextPath = false;
2732

2833
for (let i = 0; i < lines.length; i++) {
2934
const line = (lines[i] || "").trim();
@@ -41,6 +46,8 @@ export function parseHeaders(
4146
}
4247

4348
if (LINE_IS_PROBABLY_A_PATH.test(line)) {
49+
skipUntilNextPath = false;
50+
4451
if (rules.length >= maxRules) {
4552
invalid.push({
4653
message: `Maximum number of rules supported is ${maxRules}. Skipping remaining ${
@@ -74,6 +81,19 @@ export function parseHeaders(
7481
message: pathError,
7582
});
7683
rule = undefined;
84+
skipUntilNextPath = true;
85+
continue;
86+
}
87+
88+
const wildcardError = validateNoMultipleWildcards(path as string);
89+
if (wildcardError) {
90+
invalid.push({
91+
line,
92+
lineNumber: i + 1,
93+
message: wildcardError,
94+
});
95+
rule = undefined;
96+
skipUntilNextPath = true;
7797
continue;
7898
}
7999

@@ -86,6 +106,10 @@ export function parseHeaders(
86106
continue;
87107
}
88108

109+
if (skipUntilNextPath) {
110+
continue;
111+
}
112+
89113
if (!line.includes(HEADER_SEPARATOR)) {
90114
if (!rule) {
91115
invalid.push({
@@ -174,3 +198,24 @@ export function parseHeaders(
174198
function isValidRule(rule: HeadersRule) {
175199
return Object.keys(rule.headers).length > 0 || rule.unsetHeaders.length > 0;
176200
}
201+
202+
/**
203+
* At runtime, `*` wildcards are converted to `:splat` placeholders. This means
204+
* a path with multiple wildcards, or a wildcard combined with an explicit
205+
* `:splat` placeholder, would result in duplicate `:splat` parameters which is
206+
* unsupported.
207+
*/
208+
function validateNoMultipleWildcards(path: string): string | undefined {
209+
const wildcardCount = (path.match(SPLAT_REGEX) ?? []).length;
210+
const hasSplatPlaceholder = /:splat(?!\w)/.test(path);
211+
212+
if (wildcardCount > 1) {
213+
return `Only one wildcard is allowed per rule. Use a named placeholder (e.g. :project) instead. Skipping ${path}.`;
214+
}
215+
216+
if (wildcardCount > 0 && hasSplatPlaceholder) {
217+
return `Cannot combine a wildcard * with a :splat placeholder because wildcards are converted to :splat at runtime. Skipping ${path}.`;
218+
}
219+
220+
return undefined;
221+
}

packages/workers-shared/utils/tests/parseHeaders.invalid.test.ts

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,93 @@ test("parseHeaders should reject any rules after the first 100", ({
124124
});
125125
});
126126

127+
test("parseHeaders should reject paths with multiple wildcards", ({
128+
expect,
129+
}) => {
130+
const input = `
131+
# Multiple wildcards in an absolute URL
132+
https://*.pages.dev/*
133+
x-custom: value
134+
# Multiple wildcards in a relative path
135+
/blog/*/posts/*
136+
x-custom: value
137+
# Single wildcard is fine
138+
https://*.pages.dev/
139+
x-custom: value
140+
/blog/*
141+
x-custom: value
142+
`;
143+
const result = parseHeaders(input);
144+
expect(result).toEqual({
145+
rules: [
146+
{
147+
path: "https://*.pages.dev/",
148+
headers: { "x-custom": "value" },
149+
unsetHeaders: [],
150+
},
151+
{
152+
path: "/blog/*",
153+
headers: { "x-custom": "value" },
154+
unsetHeaders: [],
155+
},
156+
],
157+
invalid: [
158+
{
159+
line: "https://*.pages.dev/*",
160+
lineNumber: 3,
161+
message:
162+
"Only one wildcard is allowed per rule. Use a named placeholder (e.g. :project) instead. Skipping https://*.pages.dev/*.",
163+
},
164+
{
165+
line: "/blog/*/posts/*",
166+
lineNumber: 6,
167+
message:
168+
"Only one wildcard is allowed per rule. Use a named placeholder (e.g. :project) instead. Skipping /blog/*/posts/*.",
169+
},
170+
],
171+
});
172+
});
173+
174+
test("parseHeaders should reject paths combining wildcard with :splat placeholder", ({
175+
expect,
176+
}) => {
177+
const input = `
178+
# Wildcard + :splat in an absolute URL
179+
https://*.pages.dev/:splat
180+
x-custom: value
181+
# Wildcard + :splat in a relative path
182+
/blog/*/:splat
183+
x-custom: value
184+
# Just :splat without wildcard is fine
185+
/blog/:splat
186+
x-custom: value
187+
`;
188+
const result = parseHeaders(input);
189+
expect(result).toEqual({
190+
rules: [
191+
{
192+
path: "/blog/:splat",
193+
headers: { "x-custom": "value" },
194+
unsetHeaders: [],
195+
},
196+
],
197+
invalid: [
198+
{
199+
line: "https://*.pages.dev/:splat",
200+
lineNumber: 3,
201+
message:
202+
"Cannot combine a wildcard * with a :splat placeholder because wildcards are converted to :splat at runtime. Skipping https://*.pages.dev/:splat.",
203+
},
204+
{
205+
line: "/blog/*/:splat",
206+
lineNumber: 6,
207+
message:
208+
"Cannot combine a wildcard * with a :splat placeholder because wildcards are converted to :splat at runtime. Skipping /blog/*/:splat.",
209+
},
210+
],
211+
});
212+
});
213+
127214
test("parseHeaders should reject malformed URLs", ({ expect }) => {
128215
const input = `
129216
# Spaces should be URI encoded
@@ -167,33 +254,18 @@ test("parseHeaders should reject malformed URLs", ({ expect }) => {
167254
message:
168255
'URLs should either be relative (e.g. begin with a forward-slash), or use HTTPS (e.g. begin with "https://").',
169256
},
170-
{
171-
line: "invalid: things",
172-
lineNumber: 17,
173-
message: "Path should come before header (invalid: things)",
174-
},
175257
{
176258
line: "https://nah.com:8080",
177259
lineNumber: 19,
178260
message:
179261
"Specifying ports is not supported. Skipping absolute URL https://nah.com:8080.",
180262
},
181-
{
182-
line: "invalid: also",
183-
lineNumber: 20,
184-
message: "Path should come before header (invalid: also)",
185-
},
186263
{
187264
line: "https://nah.com:8080/blog",
188265
lineNumber: 21,
189266
message:
190267
"Specifying ports is not supported. Skipping absolute URL https://nah.com:8080/blog.",
191268
},
192-
{
193-
line: "invalid: 2",
194-
lineNumber: 22,
195-
message: "Path should come before header (invalid: 2)",
196-
},
197269
{
198270
line: "nah.com",
199271
lineNumber: 30,

0 commit comments

Comments
 (0)