fix: secure SSR cache key generation against untrusted headers#21383
fix: secure SSR cache key generation against untrusted headers#21383l3tchupkt wants to merge 2 commits intoSAP:developfrom
Conversation
- Use path-only cache key by default to prevent poisoning via X-Forwarded-Host. - Add SsrOptimizationOptions: useHostInCacheKey and allowedHosts. - Introduce getSafeHost utility with RFC-compliant validation and normalization. - Support safe subdomain matching and enforce hostname length limits. - Update tests to verify protection against cache fragmentation and hijacking.
There was a problem hiding this comment.
Pull request overview
Hardens SSR cache key generation to reduce cache poisoning / fragmentation risk by moving away from untrusted host-related headers, while introducing an allowlisted opt-in for host-based keys.
Changes:
- Added
useHostInCacheKeyandallowedHostsoptions for controlled host-based cache keys. - Updated
OptimizedSsrEngine.getRenderingKey()to prefer a host-independent key by default, with optional allowlisted host prefixing. - Added
getSafeHost()utility and expanded SSR engine tests around cache key behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| core-libs/setup/ssr/optimized-engine/ssr-optimization-options.ts | Adds new SSR options for host-based cache keys and default useHostInCacheKey: false. |
| core-libs/setup/ssr/optimized-engine/optimized-ssr-engine.ts | Updates cache key generation logic and integrates getSafeHost(). |
| core-libs/setup/ssr/optimized-engine/optimized-ssr-engine.spec.ts | Updates logging assertions and adds security-focused cache key tests. |
| core-libs/setup/ssr/express-utils/express-request-safe-host.ts | Introduces allowlist-based host extraction/validation helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
aecb077 to
0bc1d3c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
0bc1d3c to
4921c08
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| expect(consoleLogSpy.mock.lastCall[0]).toContain( | ||
| '[spartacus] SSR optimization engine initialized' | ||
| ); |
There was a problem hiding this comment.
These assertions now only check the first console.log argument, but OptimizedSsrEngine.logOptions() also logs a context object with the resolved options. Keeping an assertion on the second argument shape (e.g. that it contains an options object) would prevent regressions in initialization logging behavior.
| expect(consoleLogSpy.mock.lastCall[0]).toContain( | |
| '[spartacus] SSR optimization engine initialized' | |
| ); | |
| expect(consoleLogSpy.mock.lastCall).toEqual([ | |
| expect.stringContaining( | |
| '[spartacus] SSR optimization engine initialized' | |
| ), | |
| expect.objectContaining({ | |
| options: expect.any(Object), | |
| }), | |
| ]); |
4921c08 to
cd81d57
Compare
- Fix unreachable secure logic in getRenderingKey by removing default resolver. - Correct JSDocs for host-based cache keys and getSafeHost fallback behavior. - Fix case-sensitivity in TestEngineRunner request mock. - Resolve type errors in default optimization options.
cd81d57 to
ac3599e
Compare
|
|
2 similar comments
|
|
|
|
| if (this.ssrOptions?.renderKeyResolver) { | ||
| return this.ssrOptions.renderKeyResolver(request); | ||
| } | ||
|
|
||
| // SECURITY: Do not use X-Forwarded-Host directly as it is user-controlled. | ||
| if (this.ssrOptions?.useHostInCacheKey) { | ||
| const host = getSafeHost(request, this.ssrOptions.allowedHosts); | ||
| if (host) { | ||
| return `${host}:${request.originalUrl}`; | ||
| } | ||
| } | ||
|
|
||
| // Secure default: ignore host entirely | ||
| return request.originalUrl; |
There was a problem hiding this comment.
Thank you @l3tchupkt for your feedback and for providing the PR. Much appreciated!
- Do you really think it should be a responsibility of Spartacus to filter X-Forwarded-Host headers? What other alternatives did you considered and what pros,cons,risks of them can you see. In particular, did you consider filtering X-Forwarded-Host it in CCV2 reverse-proxy? AFAIK it's already a responsibility of CCV2 reverse proxy to set X-Forwarded-Host by default. Only in case of using CDN, CCV2 should trust the X-Forwarded-Host set by the CDN.
- This PR introduces a breaking change new behavior: the
originis no longer a part of the cache key (i.e.ssrOptions.useHostInCacheKeyis false by default). It's a breaking change behavior for existing multi-site storefronts, which handle traffic from multiple domains. Is it justified to change the default behavior? - If we'd stay with the old behavior (keeping
useHostInCacheKey: trueby default, the next breaking change is requiring from storefront owners to hardcode all their supported domains inallowedHostsin the source code (config) of Spartacus. For storefronts having multiple domains (multi-site), and multiple environments (development,staging,production) it would mean hardcoding them all (for all envs) in the built code, right? Or I'm missing something. Do you think it's an optimal approach? What alternative options can you see, their pros,cons,risks?
PS. I'm just curious: what is your main usecase in your projects: multi-site (multi-domain) or single domain? And are you using CCV2 hosting or a custom one?
|
Hi Krzysztof,
Thanks for the detailed feedback—I really appreciate the perspective.
My main intention with this PR was to address the underlying vulnerability
regarding cache poisoning via untrusted host headers. The proposed change
is one possible way to mitigate this risk while maintaining flexibility for
multi-site setups.
I am not strongly opinionated on enforcing this specific approach. My goal
was to surface the issue and provide a fix that is secure by default. If
there is a preferred direction, such as relying on CCV2/proxy guarantees or
adjusting the default behavior, I am happy to adapt the implementation
accordingly.
I look forward to your thoughts on the best way to proceed.
Best regards,
letchupkt
|
This PR addresses a security issue in the Spartacus SSR engine where the cache key could be influenced by untrusted request headers (notably
X-Forwarded-Host). This could allow cache poisoning and cache fragmentation, which may contribute to SSR resource exhaustion.Key Changes
Secure Default
The SSR cache key now uses only
req.originalUrlby default, ignoring all host-related headers. This removes host-based attack surface unless explicitly enabled.Configurable Host-Based Keys
Added
useHostInCacheKeyandallowedHostsoptions toSsrOptimizationOptionsto support multi-domain setups in a controlled way.Robust Host Validation (
getSafeHost)a.com,b.com)Safe Fallback
If no trusted host is identified, the system falls back to a host-independent cache key (
originalUrl), avoiding use of untrusted input.Security Impact
This change mitigates:
Verification
Expanded test suite with security-focused coverage:
X-Forwarded-HostandHostheadersUpdated brittle initialization snapshots in the existing test suite