Skip to content

fix: secure SSR cache key generation against untrusted headers#21383

Open
l3tchupkt wants to merge 2 commits intoSAP:developfrom
l3tchupkt:fix/secure-ssr-cache-key
Open

fix: secure SSR cache key generation against untrusted headers#21383
l3tchupkt wants to merge 2 commits intoSAP:developfrom
l3tchupkt:fix/secure-ssr-cache-key

Conversation

@l3tchupkt
Copy link
Copy Markdown

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.originalUrl by default, ignoring all host-related headers. This removes host-based attack surface unless explicitly enabled.

  • Configurable Host-Based Keys
    Added useHostInCacheKey and allowedHosts options to SsrOptimizationOptions to support multi-domain setups in a controlled way.

  • Robust Host Validation (getSafeHost)

    • Normalization: lowercasing, port stripping, trailing dot removal
    • Validation: strict hostname pattern enforcement (labels start/end with alphanumeric characters)
    • Allowlist enforcement: only explicitly allowed hosts are accepted
    • Subdomain support: allows valid subdomains of allowlisted domains while preventing bypasses
    • Length limits: enforces a 255-character maximum
    • Header sanitization: safely handles multi-value headers (e.g. 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:

  • SSR cache poisoning via host header manipulation
  • Cache fragmentation caused by attacker-controlled host values
  • Cache key manipulation through proxy headers

Verification

  • Expanded test suite with security-focused coverage:

    • Prevention of cache fragmentation from attacker-controlled host values
    • Normalization bypass checks (case, ports, trailing dots)
    • Strict allowlist enforcement for both X-Forwarded-Host and Host headers
    • Rejection of malformed or invalid hostname inputs
  • Updated brittle initialization snapshots in the existing test suite

- 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.
@l3tchupkt l3tchupkt requested a review from a team as a code owner April 20, 2026 13:57
Copilot AI review requested due to automatic review settings April 20, 2026 13:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 useHostInCacheKey and allowedHosts options 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.

Comment thread core-libs/setup/ssr/optimized-engine/optimized-ssr-engine.ts
Comment thread core-libs/setup/ssr/optimized-engine/ssr-optimization-options.ts
Comment thread core-libs/setup/ssr/express-utils/express-request-safe-host.ts Outdated
Comment thread core-libs/setup/ssr/optimized-engine/optimized-ssr-engine.spec.ts
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread core-libs/setup/ssr/optimized-engine/optimized-ssr-engine.spec.ts
Comment thread core-libs/setup/ssr/optimized-engine/ssr-optimization-options.ts
Comment thread core-libs/setup/ssr/express-utils/express-request-safe-host.ts Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +187 to +189
expect(consoleLogSpy.mock.lastCall[0]).toContain(
'[spartacus] SSR optimization engine initialized'
);
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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),
}),
]);

Copilot uses AI. Check for mistakes.
Comment thread core-libs/setup/ssr/optimized-engine/optimized-ssr-engine.spec.ts Outdated
Comment thread core-libs/setup/ssr/express-utils/express-request-safe-host.ts
Comment thread core-libs/setup/ssr/optimized-engine/optimized-ssr-engine.spec.ts Outdated
Comment thread core-libs/setup/ssr/optimized-engine/optimized-ssr-engine.spec.ts Outdated
@l3tchupkt l3tchupkt force-pushed the fix/secure-ssr-cache-key branch from 4921c08 to cd81d57 Compare April 20, 2026 15:07
- 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.
@l3tchupkt l3tchupkt force-pushed the fix/secure-ssr-cache-key branch from cd81d57 to ac3599e Compare April 20, 2026 15:11
@cla-assistant
Copy link
Copy Markdown

cla-assistant Bot commented Apr 20, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

2 similar comments
@cla-assistant
Copy link
Copy Markdown

cla-assistant Bot commented Apr 20, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@cla-assistant
Copy link
Copy Markdown

cla-assistant Bot commented Apr 20, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Comment on lines +113 to +126
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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @l3tchupkt for your feedback and for providing the PR. Much appreciated!

  1. 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.
  2. This PR introduces a breaking change new behavior: the origin is no longer a part of the cache key (i.e. ssrOptions.useHostInCacheKey is 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?
  3. If we'd stay with the old behavior (keeping useHostInCacheKey: true by default, the next breaking change is requiring from storefront owners to hardcode all their supported domains in allowedHosts in 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?

@l3tchupkt
Copy link
Copy Markdown
Author

l3tchupkt commented Apr 22, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants