Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 2 additions & 3 deletions packages/angular/ssr/src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function validateUrl(url: URL, allowedHosts: ReadonlySet<string>): void {

/**
* Sanitizes the proxy headers of a request by removing unallowed `X-Forwarded-*` headers.
* If no headers need to be removed, the original request is returned without cloning.
* If no headers need to be removed, the original request is returned unchanged.
*
* @param request - The incoming `Request` object to sanitize.
* @param trustProxyHeaders - A set of allowed proxy headers.
Expand Down Expand Up @@ -117,8 +117,7 @@ export function sanitizeRequestHeaders(
}

return headersDeleted
? new Request(request.clone(), {
signal: request.signal,
? new Request(request, {
headers,
})
: request;
Expand Down
35 changes: 35 additions & 0 deletions packages/angular/ssr/test/utils/validation_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,5 +493,40 @@ describe('Validation Utils', () => {
expect(secured.headers.get('host')).toBe('example.com');
expect(secured.headers.get('forwarded')).toBe('host=proxy.com;proto=https');
});

it('should transfer request body without teeing when removing unallowed headers', async () => {
const req = new Request('http://example.com', {
method: 'POST',
body: 'test body',
headers: {
'host': 'example.com',
'x-forwarded-host': 'evil.com',
},
});

const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(undefined));

// In the Fetch specification, calling `request.clone()` tees the body stream and leaves
// `req.bodyUsed` as `false`. Passing `req` directly to `new Request(req, ...)` transfers the
// underlying stream without teeing, immediately marking `req.bodyUsed` as `true`.
expect(req.bodyUsed).toBeTrue();
expect(await secured.text()).toBe('test body');
});

it('should preserve abort signal when removing unallowed headers', () => {
const controller = new AbortController();
const req = new Request('http://example.com', {
signal: controller.signal,
headers: {
'host': 'example.com',
'x-forwarded-host': 'evil.com',
},
});

const secured = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(undefined));

controller.abort();
expect(secured.signal.aborted).toBeTrue();
});
});
});