Skip to content

fix: reject unsafe configurable AI promotion headers - #974

Merged
leseb merged 18 commits into
praxis-proxy:mainfrom
mkoushni:fix/554-model-rewrite-promotion-headers
Sep 9, 2026
Merged

fix: reject unsafe configurable AI promotion headers#974
leseb merged 18 commits into
praxis-proxy:mainfrom
mkoushni:fix/554-model-rewrite-promotion-headers

Conversation

@mkoushni

@mkoushni mkoushni commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Configurable promotion headers could receive a client-derived model (or other body fact) on transport, credential, and other Praxis routing headers. openai_responses_model_rewrite was the original #554 case (content-length, host, hop-by-hop); the same class of config is now rejected on every body-derived promotion sink.

  • Reject hop-by-hop, framing, Host, credential, and provider API-key names (authorization, x-api-key, api-key, x-goog-api-key, …) at config load.
  • Pin each fact to its dedicated x-praxis-* header (or a custom non-x-praxis-* name). Model rewrite cannot overwrite x-praxis-ai-format; format model cannot overwrite format/rewrite facts.
  • Apply the same validator to Responses and Anthropic format classifiers, model_to_header, A2A, and MCP.
  • Share is_credential_header with inference-fixture sanitization so the denylist cannot drift.
  • Reject duplicate original/effective rewrite headers. Fail closed on unsafe targets (no parse-skip).

Default dedicated headers (x-praxis-ai-effective-model, x-praxis-ai-format, X-Model, A2A/MCP defaults) and forward_headers for Authorization are unchanged.

Related issue

Closes #554

Validation

  • Unit tests
  • Integration or functional tests
  • make lint

Covered by promotion, openai_responses_model_rewrite, openai_responses_format, anthropic_messages_format, model_to_header, A2A, MCP, and fixture header_policy unit tests. cargo clippy -p praxis-ai-apis -p praxis-ai-filters -p praxis-test-utils --lib --tests -- -D warnings passed. cargo xtask lint-filter-docs is clean. No new example config is required; existing defaults and format: x-praxis-ai-format examples remain valid.

Checklist

  • I reviewed every changed line and can explain the change.
  • New capabilities include an example config and functional example test.
  • User-facing behavior and generated documentation are updated.
  • Performance-sensitive changes include appropriate benchmark or load-test evidence.
  • Commits are signed and include a Signed-off-by trailer.

Breaking changes

Configs that name a transport, credential, API-key, or other internal x-praxis-* header as a promotion target now fail at filter construction. Dedicated defaults and custom non-x-praxis-* names continue to work. Using the same header for both rewrite effective_model and original_model is also rejected.

…xis-proxy#554)

Configurable effective/original model headers could previously name
Content-Length, Host, or hop-by-hop headers and conflict with framing.
Share the blocked-name check with API-client forward-header validation
and reject duplicate promotion names.

Signed-off-by: mkoushni <mkoushni@redhat.com>
@mkoushni
mkoushni requested review from a team and aslakknutsen September 7, 2026 13:54
The postgres vLLM job co-locates Postgres, vLLM, and OGX; the shipped
chat file-search example still uses a 60s IRR step and 5s callout, so a
slow CPU inference can 504 before the 300s OpenAI client timeout.
Match the native file-search template deadlines in the CI-only patcher.

Signed-off-by: mkoushni <mkoushni@redhat.com>
Compose the promotion-header denylist on http_hop plus Host and
Content-Length after outbound hop-by-hop sanitization landed on main.

Signed-off-by: mkoushni <mkoushni@redhat.com>

@leseb leseb left a comment

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.

P1: promotion.rs:29 still permits authorization and unrelated internal x-praxis-* targets. A client model such as Bearer attacker can overwrite upstream credentials or routing facts.
P1: The class-level fix is missing from Anthropic config:122 and Responses-format config:112, which retain syntax-only validation before promoting body-derived values. Issue #554 explicitly requires these sibling sinks to be fixed.
P2: Generated documentation:17 omits the new requirement that original/effective header names must differ.

…-promotion-headers

Signed-off-by: mkoushni <mkoushni@redhat.com>
Block Authorization and x-praxis-* names outside the AI fact namespaces
so a client model cannot overwrite credentials or routing. Apply the same
validator to Responses and Anthropic format classifiers, and document
that original/effective rewrite headers must differ.

Signed-off-by: mkoushni <mkoushni@redhat.com>
@mkoushni

mkoushni commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

> P1: promotion.rs:29 still permits authorization and unrelated internal x-praxis-* targets. A client model such as Bearer attacker can overwrite upstream credentials or routing facts. P1: The class-level fix is missing from Anthropic config:122 and Responses-format config:112, which retain syntax-only validation before promoting body-derived values. Issue #554 explicitly requires these sibling sinks to be fixed. P2: Generated documentation:17 omits the new requirement that original/effective header names must differ.

P1 credentials / internal headers. Promotion targets now reject authorization/cookie/set-cookie/www-authenticate and x-praxis-* names outside the AI fact namespaces (x-praxis-ai-, x-praxis-responses-). Defaults such as x-praxis-ai-effective-model and x-praxis-responses-mode stay allowed. forward_headers still uses only the transport list, so Authorization can still be forwarded to APIs.

P1 sibling sinks. openai_responses_format and anthropic_messages_format use the same validate_promotion_header helper.

P2 docs. Generated model-rewrite docs now say effective_model and original_model must use distinct names.

CI clippy -D unwrap_used failed on unwrap_err in the new authorization
rejection test. Match the crate's existing test-module suppressions.

Signed-off-by: mkoushni <mkoushni@redhat.com>
…-promotion-headers

Signed-off-by: mkoushni <mkoushni@redhat.com>
…-promotion-headers

Signed-off-by: mkoushni <mkoushni@redhat.com>
@mkoushni
mkoushni requested a review from leseb September 8, 2026 10:59

@leseb leseb left a comment

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.

P1 — Credential headers remain promotable. promotion.rs:64 omits x-api-key, api-key, and x-goog-api-key, although the repository explicitly classifies them as credentials in header_policy.rs:168. A client-controlled model can therefore replace an upstream API key.

P1 — Cross-purpose routing headers remain promotable. promotion.rs:68 permits every x-praxis-ai-* and x-praxis-responses-* destination. For example, configuring effective_model: x-praxis-ai-format writes a client-derived model into the format-routing fact through model_rewrite/mod.rs:411.

…-promotion-headers

Signed-off-by: mkoushni <mkoushni@redhat.com>
Model rewrite could still share AI fact namespaces, and sibling sinks
only syntax-checked header names. Require each fact's dedicated header
or a custom non-x-praxis name, share the credential denylist with
fixture sanitization, and fail closed on unsafe targets.

Signed-off-by: mkoushni <mkoushni@redhat.com>
…-promotion-headers

Signed-off-by: mkoushni <mkoushni@redhat.com>
@mkoushni mkoushni changed the title fix: reject transport headers as model-rewrite promotion targets fix: reject unsafe configurable AI promotion headers Sep 8, 2026
@mkoushni
mkoushni requested a review from leseb September 8, 2026 15:06

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Security Hardening Review

Solid security fix. The centralized denylist in promotion.rs is well-structured, the dedicated-allowlist pattern prevents cross-fact overwrites, and test coverage is thorough across all six call sites. Three items below.

Comment thread apis/src/openai/responses/config.rs Outdated
Comment thread apis/src/promotion.rs Outdated
Comment thread apis/src/openai/api_client/url.rs Outdated

@leseb leseb left a comment

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.

LGMT, just need to address the bot review. Thanks!

…-promotion-headers

Signed-off-by: mkoushni <mkoushni@redhat.com>
Shared fields cannot share a destination header. Config validation
lowercases once, and the forward-header path uses the lowercase
transport check so it does not allocate per request.

Signed-off-by: mkoushni <mkoushni@redhat.com>
…-promotion-headers

Signed-off-by: mkoushni <mkoushni@redhat.com>
@mkoushni
mkoushni requested a review from leseb September 9, 2026 10:47
@leseb
leseb added this pull request to the merge queue Sep 9, 2026
Merged via the queue into praxis-proxy:main with commit 8e9399a Sep 9, 2026
25 checks passed
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.

Configurable AI promotion headers can target transport and credential headers

3 participants