Skip to content

fix(destination): do not forward secret headers across HTTP redirects - #421

Open
tiagoek wants to merge 3 commits into
mainfrom
fix/destination-no-redirect-credential-leak
Open

tiagoek wants to merge 3 commits into
mainfrom
fix/destination-no-redirect-credential-leak

Conversation

@tiagoek

@tiagoek tiagoek commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Disclaimer: Do not include SAP-internal or customer-specific information in this PR (e.g. internal system URLs, customer names, tenant IDs, or confidential configurations). This is a public repository.

Description

DestinationHttpClient pre-baked destination credentials (auth tokens, API keys, x-sap-* headers) into a persistent requests.Session. Because requests follows 3xx redirects by default, those headers were silently forwarded to the redirect target — potentially a different origin, leaking credentials.

The default is now RedirectMode.NONE — redirects are not followed. This is automatic: no changes are required in consuming code.

New RedirectMode.SAME_ORIGIN opt-in follows same-origin relative redirects only (same scheme + host + effective port). Cross-origin, port-change, HTTPS→HTTP downgrades, and userinfo in Location are rejected. Hop count is bounded at 5. HTTP 303 converts to GET and drops the body.

HttpClient._execute() (internal SAP service client used by Destination, AI Core, and others) also sets allow_redirects=False.

allow_redirects=True in **kwargs is silently stripped — use redirect_mode instead.

RedirectMode is now exported from sap_cloud_sdk.destination.

Related Issue

No linked issue — this is a security fix.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • Breaking change (fix or feature that would cause existing functionality to change)

How to Test

  1. Run the new redirect unit tests:
    .venv/bin/python -m pytest tests/destination/unit/test_destination_http_client.py -v -k "Redirect"
  2. Verify all existing destination and core unit tests remain green:
    .venv/bin/python -m pytest tests/destination/unit/ tests/core/unit/ -v
  3. Confirm RedirectMode is importable from the public API:
    from sap_cloud_sdk.destination import RedirectMode
    assert RedirectMode.NONE.value == "none"
    assert RedirectMode.SAME_ORIGIN.value == "same_origin"
  4. Confirm a DestinationHttpClient constructed without arguments returns a 3xx response as-is instead of following it.

Checklist

  • I have read the Contributing Guidelines
  • I have verified that my changes solve the issue
  • I have added/updated automated tests to cover my changes
  • All tests pass locally
  • I have verified that my code follows the Code Guidelines
  • I have updated documentation (if applicable)
  • I have added type hints for all public APIs
  • My code does not contain sensitive information (credentials, tokens, etc.)
  • I have followed Conventional Commits for commit messages
  • (New modules only) I have attached e2e evidence showing the module working against a real BTP service
  • (Security-sensitive changes only) I have run /security-review and addressed all ❌ findings

Breaking Changes

What changes: DestinationHttpClient.request() previously followed HTTP redirects transparently (default requests behavior). After this change, redirect responses (3xx) are returned directly without following the Location header.

Impact: Any application that relied on the SDK transparently following redirects will now receive the 3xx response. Inspection of production call sites confirmed zero consumers use request() or allow_redirects, so real-world impact is minimal.

Migration path: To restore same-origin redirect following:

from sap_cloud_sdk.destination import DestinationHttpClient, RedirectMode

http = DestinationHttpClient(dest, redirect_mode=RedirectMode.SAME_ORIGIN)

Alternatives considered: Header-stripping on cross-origin hops — rejected because it creates a selective allow-list that is easy to misconfigure and still leaks non-Authorization headers (e.g. apiKey, x-sap-security-session).

Additional Notes

This PR was developed with AI assistance (Claude). See the SAP AI contribution guideline for the applicable requirements.

What changed

  • _destination_http_client.py: RedirectMode enum, _parse_origin helper, redirect enforcement in request(), _request_following_same_origin() private method
  • __init__.py: exports RedirectMode
  • core/protocol/http/client.py: allow_redirects=False in _execute()
  • user-guide.md: new "Redirect behavior (security boundary)" section
  • CHANGELOG.md: new file documenting the behavioral change under [Unreleased]
  • pyproject.toml: version unchanged (managed by release pipeline)

Test coverage

  • 22 new unit tests: NONE/SAME_ORIGIN, all 3xx codes, credential non-leakage, redirect chains, HTTPS→HTTP rejection, 303 body drop, allow_redirects kwarg stripping
  • 1 new test for HttpClient._execute() asserting allow_redirects=False
  • All existing destination and core unit tests remain green
  • BDD integration test unaffected (httpbin.org echo path, no redirect)

DestinationHttpClient pre-bakes destination credentials (auth tokens,
API keys, x-sap-* headers) into a persistent requests.Session. Because
requests followed 3xx redirects automatically, those headers were silently
forwarded to the redirect target — potentially a different origin.

Changes:
- DestinationHttpClient.request() now defaults to RedirectMode.NONE, which
  returns the 3xx response as-is without following it. This is automatic:
  no changes are required in consuming code.
- New RedirectMode.SAME_ORIGIN opt-in follows redirects only when the
  Location URL has the same scheme, host, and effective port. Cross-origin,
  port-change, HTTPS→HTTP, and userinfo redirects are rejected. Bounded
  at 5 hops. 303 See Other converts to GET and drops the body.
- allow_redirects kwarg is silently stripped from request() — redirect
  behavior is controlled only through redirect_mode.
- HttpClient._execute() (internal SAP service client) also sets
  allow_redirects=False to protect per-request Authorization headers.
- RedirectMode exported from sap_cloud_sdk.destination.

Tests: 22 new cases covering NONE/SAME_ORIGIN, all 3xx codes, credential
non-leakage, chain limits, HTTPS→HTTP rejection, and 303 body drop.
- Revert pyproject.toml version 0.63.0 → 0.62.0 (version managed by release pipeline)
- Update CHANGELOG to [Unreleased] section (version assigned at release time)
- Fix ruff-format: expand DestinationHttpClient/RedirectMode import to multi-line
@tiagoek
tiagoek marked this pull request as ready for review October 21, 2026 18:38
@tiagoek
tiagoek requested a review from a team as a code owner October 21, 2026 18:38
Remove `call` from the redirect test imports — only `call_count` and
`call_args_list` are used; `call` itself was never referenced.
tiagoek

This comment was marked as off-topic.

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant