Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-forwarded-proto-allowed-domains.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

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.
6 changes: 4 additions & 2 deletions packages/astro/src/core/app/validate-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,12 @@ export function validateForwardedHeaders(
if (allowedDomains && allowedDomains.length > 0) {
const hasProtocolPatterns = allowedDomains.some((pattern) => pattern.protocol !== undefined);
if (hasProtocolPatterns) {
// Validate against allowedDomains patterns
// Only validate the protocol here; host+proto combination is checked in the host block below
try {
const testUrl = new URL(`${forwardedProtocol}://example.com`);
const isAllowed = allowedDomains.some((pattern) => matchPattern(testUrl, pattern));
const isAllowed = allowedDomains.some((pattern) =>
matchPattern(testUrl, { protocol: pattern.protocol }),
);
if (isAllowed) {
result.protocol = forwardedProtocol;
}
Expand Down
67 changes: 67 additions & 0 deletions packages/astro/test/units/app/node.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,73 @@ describe('NodeApp', () => {
);
assert.equal(result.url, 'https://example.com/');
});

it('accepts x-forwarded-proto when allowedDomains has protocol and hostname', () => {
const result = createRequest(
{
...mockNodeRequest,
socket: { encrypted: false, remoteAddress: '2.2.2.2' },
headers: {
host: 'myapp.example.com',
'x-forwarded-proto': 'https',
},
},
{ allowedDomains: [{ protocol: 'https', hostname: 'myapp.example.com' }] },
);
// Without the fix, protocol validation fails due to hostname mismatch
// and falls back to socket.encrypted (false → http)
assert.equal(result.url, 'https://myapp.example.com/');
});

it('rejects x-forwarded-proto when it does not match protocol in allowedDomains', () => {
const result = createRequest(
{
...mockNodeRequest,
socket: { encrypted: false, remoteAddress: '2.2.2.2' },
headers: {
host: 'myapp.example.com',
'x-forwarded-proto': 'http',
},
},
{ allowedDomains: [{ protocol: 'https', hostname: 'myapp.example.com' }] },
);
// http is not in allowedDomains (only https), protocol falls back to socket (false → http)
// Host validation also fails because http doesn't match the pattern's protocol: 'https'
assert.equal(result.url, 'http://localhost/');
});

it('accepts x-forwarded-proto with wildcard hostname pattern in allowedDomains', () => {
const result = createRequest(
{
...mockNodeRequest,
socket: { encrypted: false, remoteAddress: '2.2.2.2' },
headers: {
host: 'myapp.example.com',
'x-forwarded-proto': 'https',
},
},
{ allowedDomains: [{ protocol: 'https', hostname: '**.example.com' }] },
);
assert.equal(result.url, 'https://myapp.example.com/');
});

it('constructs correct URL behind reverse proxy with all forwarded headers', () => {
// Simulates: Reverse proxy terminates TLS, connects to Astro via HTTP,
// forwards original protocol/host/port via X-Forwarded-* headers
const result = createRequest(
{
...mockNodeRequest,
socket: { encrypted: false, remoteAddress: '2.2.2.2' },
headers: {
host: 'myapp.example.com',
'x-forwarded-proto': 'https',
'x-forwarded-host': 'myapp.example.com',
},
},
{ allowedDomains: [{ protocol: 'https', hostname: 'myapp.example.com' }] },
);
assert.equal(result.url, 'https://myapp.example.com/');
});
});

describe('x-forwarded-port', () => {
Expand Down
Loading