fix(guardrails): fail closed on NeMo redact verdict until body replac… - #721
fix(guardrails): fail closed on NeMo redact verdict until body replac…#721mkoushni wants to merge 6 commits into
Conversation
No — the fix is still needed. main still has the original vulnerable code: GuardResult::Redact { reason, .. } => { My fix (FilterAction::Reject(403)) is still the correct interim solution. The PR should go in as-is. I will fix the errors in the tests |
8fd8f4c to
946e2be
Compare
praxis-bot
left a comment
There was a problem hiding this comment.
Security fix is correct: GuardResult::Redact now fails closed with 403 instead of silently forwarding the original body containing sensitive data. The status recording order in record_verdict (write "redacted" before the match) ensures filter_results remains accurate even on rejection. Unit tests, regression test, and integration test all updated consistently. Two medium items below.
praxis-bot
left a comment
There was a problem hiding this comment.
Security fix is correct and well-scoped. GuardResult::Redact now fails closed with HTTP 403, matching the Block path, so the original unmasked body never reaches the upstream. The reason field passed to Rejection::with_body comes from blocked_rail_names() (rail names only, not sensitive content), so no information leak in the 403 body. Status recording ("redacted") happens before the match arms, keeping filter_results accurate for downstream branch logic even on rejection. Regression test and integration test cover the critical invariant. One medium item below (not a duplicate of the prior review, which flagged the same class of issue in the integration test).
0d529ea to
e6f41c0
Compare
praxis-bot
left a comment
There was a problem hiding this comment.
The core security fix in record_verdict is correct and well-scoped: GuardResult::Redact now rejects with HTTP 403, matching the Block path, so the unmodified body containing sensitive content is never forwarded upstream. The reason field passed to Rejection::with_body originates from blocked_rail_names() (rail names only, not user content), avoiding information leakage in the 403 response. Status recording ("redacted") remains accurate because it is written before the match arms. Unit tests, regression test, and integration test updated consistently. One medium item below.
…ement is ready GuardResult::Redact was returning FilterAction::Continue while recording status=redacted, silently forwarding the original unmodified body. This leaked content the configured guardrail identified as sensitive and misled downstream branch logic into treating the request as sanitised. Change record_verdict to return FilterAction::Reject(403) for GuardResult::Redact, identical to the GuardResult::Block path, so the original body is never forwarded. Body replacement with the provider's modified_text is deferred to praxis-proxy#579. Update on_request_body_modified_writes_filter_results to assert the 403 rejection and rename it accordingly. Add regression test on_request_body_modified_never_forwards_original_secret that asserts the action is Reject, the original SSN is absent from the body buffer, and status is never recorded as passed. Fixes: fnd_sig-feat-custom-ai-anthropic-gua_cfe2e666bb Signed-off-by: mkoushni <mkoushni@redhat.com>
Replace the weaker matches!(action, Reject(_)) assertion in on_request_body_modified_never_forwards_original_secret with a direct check that the raw SSN never appears in the rejection body. This makes the test meaningfully different from on_request_body_modified_rejects_with_403 and directly validates the stated invariant: the unmodified user content must not surface in any response path. Reported by praxis-bot review of fix/guardrails-redact-fail-closed. Signed-off-by: mkoushni <mkoushni@redhat.com>
e6f41c0 to
b63d55b
Compare
| // Full body replacement deferred to #579 (NeMo mask/redact action). | ||
| tracing::warn!(verdict, %reason, "ai_guardrails: provider verdict; forwarding unchanged until #579"); | ||
| Ok(FilterAction::Continue) | ||
| // Body replacement is tracked in #579. Until it is implemented, fail |
There was a problem hiding this comment.
579 is closed so why still have a comment for it? the comment is wrong here
…y-lines lint Both on_request_body_modified_rejects_with_403 and on_request_body_modified_never_forwards_original_secret share identical mock setup. Extract it into nemo_pii_redact_filter() to bring each test under the 30-line clippy limit. Also collapse the two-line rejection body setup into one expression using rejection.body.as_deref().unwrap_or_default(). Signed-off-by: mkoushni <mkoushni@redhat.com>
Collapse the split method chain onto a single line as required by cargo +nightly fmt --check. Signed-off-by: mkoushni <mkoushni@redhat.com>
leseb
left a comment
There was a problem hiding this comment.
let's resolve this https://github.com/praxis-proxy/ai/pull/721/changes#r3776143828
fix(guardrails): fail closed on NeMo redact verdict until body replacement is ready
Summary
Addresses Clawpatch finding
fnd_sig-feat-custom-ai-anthropic-gua_cfe2e666bb(severity: high / security / confidence: high).GuardResult::Redactwas returningFilterAction::Continuewhile recordingstatus=redacted, silently forwarding the original unmodified body containingsensitive content. A request with
my ssn is 123-45-6789would be passedupstream unchanged while the filter claimed it had been redacted.
Root Cause
The
modified_textfrom the provider was discarded, the original body wasforwarded, and
status=redactedwas recorded — misleading downstream branchlogic and auditing into treating the request as sanitised.
Fix
GuardResult::Redactnow follows the same code path asGuardResult::Block:reject with HTTP 403, record
status=redacted, never forward the original body.Body replacement with the provider's
modified_textremains deferred to #579.Changes
filters/src/guardrails/filter.rs:166-171GuardResult::Redact→FilterAction::Reject(403)filters/src/guardrails/tests.rsTests:
on_request_body_modified_rejects_with_403— replaces the staleon_request_body_modified_writes_filter_results; asserts HTTP 403 andstatus=redactedis still recorded even when rejecting.on_request_body_modified_never_forwards_original_secret— regression testper Clawpatch recommendation; asserts the action is
Reject(runtimediscards the body) and
statusis never recorded aspassed.Test Results
Resolve
#673