Skip to content

fix(destination): validate and encode resource names in URL path segments - #383

Draft
tiagoek wants to merge 7 commits into
mainfrom
fix/destination-name-path-traversal
Draft

tiagoek wants to merge 7 commits into
mainfrom
fix/destination-name-path-traversal

Conversation

@tiagoek

@tiagoek tiagoek commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Destination, fragment, and certificate resource names passed to public SDK methods are now validated against an allowlist grammar ([A-Za-z0-9][A-Za-z0-9._\-]{0,199}) and percent-encoded before being interpolated into URL path segments.

A ValueError is raised for names containing path separators, dot segments, query/fragment delimiters, control characters, or other characters outside the allowed set — before any OAuth token is fetched or HTTP request is sent. A defense-in-depth guard in _request() also rejects paths containing .. segments.

get_destination() auto-parses a @ConsumptionLevel suffix in the name for backward compatibility. Callers using "my-dest@provider_subaccount" continue to work without changes; the level= parameter form is the canonical usage.

No breaking changes. No agent or consumer repositories need to be modified.

Changes

  • New src/sap_cloud_sdk/destination/utils/_validation.py with validate_resource_name() and encode_path_segment() helpers
  • validate_resource_name() added as the first statement in every public name-accepting method across DestinationClient, FragmentClient, and CertificateClient
  • encode_path_segment() applied at every URL interpolation site in all three clients
  • _split_name_and_level() helper in client.py auto-parses @ConsumptionLevel suffix for backward compatibility
  • Defense-in-depth .. segment guard added to _request() in _http.py
  • 843 unit tests, 0 failures

Test plan

  • uv run pytest tests/destination/unit/test_validation.py -v — 42 cases covering allowlist accepts and rejects
  • uv run pytest tests/destination/unit/ -q — full unit suite, 843 tests green
  • TestPathTraversalGuard in test_client.py, test_fragment_client.py, test_certificate_client.py — attack names rejected, no HTTP call on invalid name, @level auto-parse backward compat

New utils/_validation.py provides:
- validate_resource_name(): allowlist check [A-Za-z0-9][A-Za-z0-9._-]{0,199}
  raises ValueError for names with path separators, dot segments, control
  chars, percent escapes, @, and non-ASCII characters
- encode_path_segment(): validates then percent-encodes for safe URL interpolation

All 42 unit tests green. Full unit suite: 735/735.
- Add _split_name_and_level() module helper to auto-parse @ConsumptionLevel
  suffix from name (backward compat for legacy callers)
- validate_resource_name() called before proxy check / try block so
  ValueError propagates cleanly without being wrapped in DestinationOperationError
- encode_path_segment() applied at every URL interpolation site (V1 + V2)
- create_destination / update_destination validate dest.name (round-trip guard)
- Deprecated get_instance_destination / get_subaccount_destination validate too

184 client tests green.
…tificateClient

Same security pattern as DestinationClient:
- validate_resource_name() at every public method boundary (before try block)
- encode_path_segment() at every URL interpolation site
- create/update validate entity.name (round-trip consistency guard)

183 fragment + certificate tests green.
…st()

Reject any path containing a '..' segment before issuing the HTTP request.
Belt-and-suspenders layer behind the client-layer name validation.
10 tests green.
…subaccount read methods

get_instance_fragment, get_subaccount_fragment, get_instance_certificate, and
get_subaccount_certificate were delegating directly to internal helpers that have an
except Exception catch-all, causing invalid names to raise DestinationOperationError
instead of ValueError. Add validate_resource_name before the try block in all four
methods, consistent with every other public method on this branch.

Also replace path-echo in _http.py traversal guard error message with a fixed generic
message to avoid returning attacker-controlled strings in error responses.

843 tests green.
- Bump version 0.58.1 → 0.58.2 (patch; src/ modified on this branch)
- Collapse multiline raise in _validation.py to satisfy ruff-format
- Add ty: ignore[invalid-argument-type] suppression on intentional
  non-string test inputs in test_validation.py
- Add resource name validation section and @Level shorthand docs to
  destination user-guide.md

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant