Skip to content

Commit efae11c

Browse files
qzioz0mt3clouisescher
authored
fix: X-Forwarded-Proto rejected when allowedDomains includes protocol… (#15594)
Co-authored-by: Timo Behrmann <timo.behrmann@gmail.com> Co-authored-by: Louis Escher <66965600+louisescher@users.noreply.github.com> fix: X-Forwarded-Proto rejected when allowedDomains includes protocol and hostname (#15560) Fixes #15559
1 parent 751ccf0 commit efae11c

File tree

3 files changed

+76
-2
lines changed

3 files changed

+76
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fix X-Forwarded-Proto validation when allowedDomains includes both protocol and hostname fields. The protocol check no longer fails due to hostname mismatch against the hardcoded test URL.

packages/astro/src/core/app/validate-headers.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,12 @@ export function validateForwardedHeaders(
9090
if (allowedDomains && allowedDomains.length > 0) {
9191
const hasProtocolPatterns = allowedDomains.some((pattern) => pattern.protocol !== undefined);
9292
if (hasProtocolPatterns) {
93-
// Validate against allowedDomains patterns
93+
// Only validate the protocol here; host+proto combination is checked in the host block below
9494
try {
9595
const testUrl = new URL(`${forwardedProtocol}://example.com`);
96-
const isAllowed = allowedDomains.some((pattern) => matchPattern(testUrl, pattern));
96+
const isAllowed = allowedDomains.some((pattern) =>
97+
matchPattern(testUrl, { protocol: pattern.protocol }),
98+
);
9799
if (isAllowed) {
98100
result.protocol = forwardedProtocol;
99101
}

packages/astro/test/units/app/node.test.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,73 @@ describe('NodeApp', () => {
430430
);
431431
assert.equal(result.url, 'https://example.com/');
432432
});
433+
434+
it('accepts x-forwarded-proto when allowedDomains has protocol and hostname', () => {
435+
const result = NodeApp.createRequest(
436+
{
437+
...mockNodeRequest,
438+
socket: { encrypted: false, remoteAddress: '2.2.2.2' },
439+
headers: {
440+
host: 'myapp.example.com',
441+
'x-forwarded-proto': 'https',
442+
},
443+
},
444+
{ allowedDomains: [{ protocol: 'https', hostname: 'myapp.example.com' }] },
445+
);
446+
// Without the fix, protocol validation fails due to hostname mismatch
447+
// and falls back to socket.encrypted (false → http)
448+
assert.equal(result.url, 'https://myapp.example.com/');
449+
});
450+
451+
it('rejects x-forwarded-proto when it does not match protocol in allowedDomains', () => {
452+
const result = NodeApp.createRequest(
453+
{
454+
...mockNodeRequest,
455+
socket: { encrypted: false, remoteAddress: '2.2.2.2' },
456+
headers: {
457+
host: 'myapp.example.com',
458+
'x-forwarded-proto': 'http',
459+
},
460+
},
461+
{ allowedDomains: [{ protocol: 'https', hostname: 'myapp.example.com' }] },
462+
);
463+
// http is not in allowedDomains (only https), protocol falls back to socket (false → http)
464+
// Host validation also fails because http doesn't match the pattern's protocol: 'https'
465+
assert.equal(result.url, 'http://localhost/');
466+
});
467+
468+
it('accepts x-forwarded-proto with wildcard hostname pattern in allowedDomains', () => {
469+
const result = NodeApp.createRequest(
470+
{
471+
...mockNodeRequest,
472+
socket: { encrypted: false, remoteAddress: '2.2.2.2' },
473+
headers: {
474+
host: 'myapp.example.com',
475+
'x-forwarded-proto': 'https',
476+
},
477+
},
478+
{ allowedDomains: [{ protocol: 'https', hostname: '**.example.com' }] },
479+
);
480+
assert.equal(result.url, 'https://myapp.example.com/');
481+
});
482+
483+
it('constructs correct URL behind reverse proxy with all forwarded headers', () => {
484+
// Simulates: Reverse proxy terminates TLS, connects to Astro via HTTP,
485+
// forwards original protocol/host/port via X-Forwarded-* headers
486+
const result = NodeApp.createRequest(
487+
{
488+
...mockNodeRequest,
489+
socket: { encrypted: false, remoteAddress: '2.2.2.2' },
490+
headers: {
491+
host: 'myapp.example.com',
492+
'x-forwarded-proto': 'https',
493+
'x-forwarded-host': 'myapp.example.com',
494+
},
495+
},
496+
{ allowedDomains: [{ protocol: 'https', hostname: 'myapp.example.com' }] },
497+
);
498+
assert.equal(result.url, 'https://myapp.example.com/');
499+
});
433500
});
434501

435502
describe('x-forwarded-port', () => {

0 commit comments

Comments
 (0)