Skip to content

fix(agent-os): redact secrets glued to a following suffix - #3853

Merged
MohammadHaroonAbuomar merged 3 commits into
microsoft:mainfrom
qubeena07:fix/3494-credential-redactor-trailing-suffix
Sep 14, 2026
Merged

MohammadHaroonAbuomar merged 3 commits into
microsoft:mainfrom
qubeena07:fix/3494-credential-redactor-trailing-suffix

Conversation

@qubeena07

Copy link
Copy Markdown
Contributor

Related Issue

Fixes #3494

Timeline

None

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Maintenance (dependency updates, CI/CD, refactoring)
  • Security fix

Package(s) Affected

Core & runtime:

  • agent-governance-toolkit-core
  • agent-primitives
  • agent-os
  • agent-mesh
  • agent-runtime
  • agent-sre
  • agent-compliance

Testing

Unit Testing

Added test_redacts_secret_glued_to_a_following_word_character, covering AWS access key, all five GitHub token prefixes, Google API key, Stripe secret key and Basic auth secret with a suffix such as _old or _rotated glued directly to the secret. Added test_trailing_anchor_does_not_widen_the_match to confirm the new lookahead does not loosen the boundary: one extra character after a fixed length key, a Stripe key under the length floor, a short Basic value and a Basic auth URL with no credentials all still fail to match. Added two glued left edge cases (auth_Basic ... and url_https://user:pass@...) to the existing test_detects_secret_glued_to_preceding_word_character parametrization, since Basic auth secret still had the left edge \b bug that every other prefix anchored pattern had already moved away from. Removed one case from test_github_token_boundaries_and_lengths_avoid_false_positives that encoded the bug itself as expected behavior (a GitHub token followed by a bare underscore was asserted to produce no match at all). Added test_trailing_lookahead_patterns_handle_adversarial_input_quickly to confirm the new lookahead does not introduce backtracking on 100k character adversarial input.

pytest agent-governance-python/agent-os/tests/test_credential_redactor.py: 93 passed. Reverting only the source change while keeping the new tests: 11 of those fail, confirming they exercise the fix rather than just describing it.

Manual Testing

Ran AKIAIOSFODNN7EXAMPLE_old through CredentialRedactor.redact and contains_credentials directly before and after the change: before, the input passed through completely unredacted and detection reported nothing; after, it redacts to [REDACTED]_old and detection reports AWS access key. Repeated for the GitHub, Google, Stripe and Basic auth cases described in the linked issue.

Checklist

  • I have linked a related issue above, or completed "Problem & Solution", "Impact on Your Work", and "Alternatives Considered"
  • My code follows the project style guidelines (ruff check)
  • I have added tests that prove my fix/feature works
  • All new and existing tests pass (pytest)
  • I have updated documentation as needed
  • I have signed the Microsoft CLA

Attribution & Prior Art

  • This contribution does not contain code copied or derived from other projects without attribution
  • Any external projects that inspired this design are credited in code comments or documentation
  • If this PR implements functionality similar to an existing open-source project, I have listed it below

Prior art / related projects: none. Note for reviewers: issue #3494 previously had a fix proposed in #3495, which went through review and was closed on 2026-08-05 for a reason unrelated to the code ("this repository is not accepting submissions from this account"). This PR reaches the same diagnosis independently by reading the current pattern table in credential_redactor.py against the issue's own description, and was not copied from #3495.

AI Assistance

  • I can explain every meaningful change in this PR: what it does, why, and what tradeoffs were considered
  • I have run tests and verification appropriate for this change
  • No part of this PR was autonomously submitted by an AI agent without my review
  • I have not used AI to generate review comments on others' PRs

An AI coding assistant running in my terminal was used for the regex analysis, the test authoring and this description. I reviewed, ran and verified every change before opening this PR.

IP, Patents, and Licensing

  • This contribution does not implement patent-pending or patent-encumbered techniques
  • This contribution does not require an NDA or licensing agreement to understand or use
  • Any AI tools used have terms compatible with the MIT License

CredentialRedactor left AWS access key, GitHub token, Google API key
and Stripe secret key completely unredacted when a valid secret was
immediately followed by an annotation like _old or _rotated. Each of
those patterns has a fixed length or a value class that excludes
underscore, so a trailing \b found no shorter match to fall back on
and the whole pattern failed rather than truncating. Basic auth
secret had the same gap on its left edge, since it still used \b
there after every other prefix anchored pattern had already moved to
a lookbehind.

Replace the trailing \b with the mirror lookahead (?![A-Za-z0-9]) on
the four affected patterns, and anchor Basic auth secret on both
sides. Patterns whose value class already includes underscore
(OpenAI, Bearer, JWT) are unaffected since they already absorb the
suffix.

Fixes microsoft#3494

Signed-off-by: qubeena07 <qubeena7@gmail.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@github-actions github-actions Bot added tests size/M Medium PR (< 200 lines) labels Aug 30, 2026
@github-actions

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ⚠️ Missing No current-run comment
🛡️ Security Scan ⚠️ Missing No current-run comment
🔄 Breaking Changes ⚠️ Missing No current-run comment
📝 Docs Sync ⚠️ Missing No current-run comment
🧪 Test Coverage ⚠️ Missing No current-run comment

Verdict: ⚠️ AI review incomplete; ready for human review

AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims.

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.

Loaded credential_redactor.py from main, from this branch, and from #3855 side by side in one
process and ran the same inputs through all three, because #3855 fixes the same bug and the
difference between the two is what decides which one should land.

The bug is real and the explanation in your comment is the correct one. For a pattern whose value
class excludes _ or whose length is fixed, a trailing \b after a suffix like _old has no
shorter alternative to back off to, so the whole match fails and the complete secret passes through
unredacted rather than being truncated. That is a fail-open, and it is the worst failure mode this
module has.

Measured, contains_credentials on each tree:

input main #3853 #3855
AKIAIOSFODNN7EXAMPLE_old miss HIT HIT
ghp_<24>_old miss HIT miss
ghs_<24>_deprecated miss HIT miss
AIza<35>_v2 miss HIT HIT
sk_live_FakeTestKey0000_rotated miss HIT HIT
Basic <b64>_old miss HIT HIT
auth_Basic <b64> miss HIT HIT
url_https://alice:password@example.com/resource miss HIT HIT

And the negatives, which is the half that matters for a widening change. AKIAIOSFODNN7EXAMPLEX,
xAKIAIOSFODNN7EXAMPLE_old, AIza<35>9, sk_live_short and a plain https://example.com/resource
are all still unmatched on this branch, identically to main. The mirror assertion is exactly as
strict about what may follow as the fixed length or value class already was, so nothing widens.

The line that separates this PR from #3855 is the GitHub token one. #3855 leaves that pattern's
(?![A-Za-z0-9_]) in place, so on that branch:

redact("ghp_AAAAAAAAAAAAAAAAAAAAAAAA_old")
  main   -> ghp_AAAAAAAAAAAAAAAAAAAAAAAA_old
  #3853  -> [REDACTED]_old
  #3855  -> ghp_AAAAAAAAAAAAAAAAAAAAAAAA_old

A live GitHub PAT annotated _old, _backup or _rotated is exactly the shape a real secret takes
in a config file or a rotation note, and it stays fully in the clear under #3855. This branch is a
strict superset on every case I tried, and it is the one I would merge.

Two things I checked and want on the record. Removing f"{_fake_github_token('ghs')}_" from the
negative list is correct and not a weakened test: that string is now a real detection, so keeping it
as a negative would have asserted the bug. And the new ReDoS tests are the right instinct, since the
added lookahead is a single fixed-width assertion rather than a repeated class, so linear-time
behaviour is preserved.

Nothing blocking. Credit to #3855 for finding the same class independently; the base64-padding case
it tests (auth_Basic YWJjZGVmZ2hpaw==) already passes on this branch, so nothing is lost by
landing this one.

@imran-siddique

Copy link
Copy Markdown
Collaborator

MohammadHaroonAbuomar liamcrumm flagging for priority. This closes a fail-open in
CredentialRedactor: a secret followed by an annotation like _old or _rotated passes through
completely unredacted rather than partially. I measured it across three trees; on main,
redact("ghp_<24>_old") returns the PAT in the clear.

#3855 fixes the same class from another contributor but leaves the GitHub token pattern untouched, so
this is the one to take. Detail and the negative-case table are in my review above.

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.

Reviewed head 756773999ad0663e70e6a5836c14e8a195b68cde. I ran all 93 credential-redactor tests on Python 3.11; restoring the parent source while retaining these tests produces 11 failures. An independent 98-case matrix covered all five short GitHub token prefixes at multiple lengths, underscore annotations, AWS/Google/Stripe/Basic credentials, and negative boundaries. I also checked padded Basic auth.

I compared this head with #3855 at 721429a8. Both fix the AWS/Google/Stripe/Basic cases I exercised, but #3855 leaves underscore-suffixed GitHub tokens unredacted. This PR covers that gap. My recommendation is to land #3853 as the broader fix for #3494; this supersedes my earlier preference implied by approving #3855. Credit to both contributors for the overlapping work.

No blocking finding. Source lint passes; the test import-order warning also occurs on the parent. I verified the signoff and a conflict-free merge simulation against main 359a2332.

@MohammadHaroonAbuomar MohammadHaroonAbuomar 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.

  • Superset rule violated for the Google API key pattern: the new trailing lookahead (?![A-Za-z0-9]) misses a valid 39-char key whose 35th value char is - when an alphanumeric follows, which the old \b detected. The full key then leaves the MCP gateway unredacted under both BLOCK and SANITIZE policies, where origin/main blocked or redacted it. This is the same glued-suffix bug class the PR claims to fix. File: agent-governance-python/agent-os/src/agent_os/credential_redactor.py:153.

Dipika Ranabhat (qubeena07) added a commit to qubeena07/agent-governance-toolkit that referenced this pull request Sep 13, 2026
Review on microsoft#3934 caught a real narrowing: the previous plain \b treated
hyphen as an automatic boundary on its own, since hyphen is not a word
character, so a 35 character key ending in a hyphen and glued directly
to more text used to be redacted. The new lookahead treats that the
same as every other "one more alphanumeric character right there" case
already in this file, on the same reasoning already applied to the AWS
pattern, and leaves it unredacted. That reasoning is sound, but I had
not written it down or tested it, so it looked like an accident rather
than a choice. Added a comment and a test pinning it.

Also confirmed the Python port (PR microsoft#3853, agent-os credential_redactor.py)
has the identical gap in its Google API key pattern, and will pin it the
same way there.

Signed-off-by: qubeena07 <qubeena7@gmail.com>
… test

Review on the dotnet port of this fix, PR 3934, caught a real narrowing
that also applies here: a trailing \b would have treated hyphen as an
automatic boundary on its own, since hyphen is not a word character, so
a 35 character key ending in a hyphen and glued directly to more text
used to be redacted before the fix in this branch. The mirror assertion
treats that the same as every other "one more alphanumeric character
right there" case already covered, on the same reasoning already applied
to AWS access key. That reasoning is sound, but it was not written down
or tested, so it looked like an accident rather than a choice. Added a
comment and a test pinning it.

94 passed, tests/test_credential_redactor.py.

Signed-off-by: qubeena07 <qubeena7@gmail.com>
@qubeena07

Copy link
Copy Markdown
Contributor Author

Follow-up: review on the dotnet port of this same fix, PR #3934, caught a real narrowing in the Google API key pattern that applies here too. A trailing \b would have treated hyphen as an automatic boundary on its own, since hyphen is not a word character, so a 35 character key ending in a hyphen and glued directly to more text used to be redacted before this fix, and the mirror assertion now treats that the same as the other "one more alphanumeric character" cases already covered here, on the same reasoning already applied to AWS access key. That reasoning holds, but it was not written down or tested, so pushed a comment and a dedicated test pinning it (test_does_not_widen_google_api_key_match_when_key_ends_in_hyphen) rather than leave it as an unexamined side effect. 94 passed locally.

Comment thread agent-governance-python/agent-os/src/agent_os/credential_redactor.py Outdated
…to more text

Review on this PR pointed out that a detector change must never lose a
shape the previous version caught. The mirror assertion alone loses one:
a trailing \b treats hyphen as an automatic boundary on its own, since
hyphen is not a word character, so a 35 character key ending in a hyphen
and glued straight to more text was redacted before this branch, and the
mirror assertion by itself does not see that, since it only looks at
what follows, not at what was actually consumed.

The pattern now also accepts whenever the character actually consumed at
that position is a hyphen, on top of the existing "not alphanumeric"
check, restoring the original behavior for that one shape while keeping
the "one more alphanumeric character means a different token" guard for
every other ending. Verified against the previous pattern across a grid
of 35th characters and following characters: the only three cases that
change are exactly the ones this is meant to fix, nothing else widens.

Flips the pin test added for this same gap into a positive regression
test, since the gap is now closed rather than documented as accepted.

94 passed, tests/test_credential_redactor.py. Reverting only the source
change and keeping the flipped test: it fails, confirming the test
exercises this fix.

Signed-off-by: qubeena07 <qubeena7@gmail.com>
@qubeena07

Copy link
Copy Markdown
Contributor Author

Agreed on the strict resolution, a detector change should never lose a shape the base caught. Pushed the exact change requested: the Google tail is now (?:(?![A-Za-z0-9])|(?<=-)), and the pin test is flipped to assert the hyphen-ending case is redacted rather than documenting it as an accepted gap.

Verified locally against a grid of every 35th character (alnum, hyphen, underscore) crossed with every plausible following character, comparing this change against the previously pushed pattern: the only three cases that change are the hyphen-ending-glued-to-alnum ones, nothing else widens. 94 passed in tests/test_credential_redactor.py, and reverting only the source change while keeping the flipped test fails it, confirming it exercises the fix.

Also applied the identical change to the dotnet port, PR #3934, for consistency.

@MohammadHaroonAbuomar MohammadHaroonAbuomar 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.

Verified at c14a10d: the Google tail is now the strict-superset form and the pin test is flipped to a positive regression test. A 567k-text base-vs-head probe shows zero lost shapes across the five changed patterns and no lookalike over-redaction. All three commits signed.

@MohammadHaroonAbuomar
MohammadHaroonAbuomar merged commit 295f777 into microsoft:main Sep 14, 2026
125 checks passed
Yuvraj Singh (yuvrajsingh2428) pushed a commit to yuvrajsingh2428/agent-governance-toolkit that referenced this pull request Oct 1, 2026
…3853)

* fix(agent-os): redact secrets glued to a following suffix

CredentialRedactor left AWS access key, GitHub token, Google API key
and Stripe secret key completely unredacted when a valid secret was
immediately followed by an annotation like _old or _rotated. Each of
those patterns has a fixed length or a value class that excludes
underscore, so a trailing \b found no shorter match to fall back on
and the whole pattern failed rather than truncating. Basic auth
secret had the same gap on its left edge, since it still used \b
there after every other prefix anchored pattern had already moved to
a lookbehind.

Replace the trailing \b with the mirror lookahead (?![A-Za-z0-9]) on
the four affected patterns, and anchor Basic auth secret on both
sides. Patterns whose value class already includes underscore
(OpenAI, Bearer, JWT) are unaffected since they already absorb the
suffix.

Fixes microsoft#3494

Signed-off-by: qubeena07 <qubeena7@gmail.com>

* fix(agent-os): pin the Google API key hyphen boundary tradeoff with a test

Review on the dotnet port of this fix, PR 3934, caught a real narrowing
that also applies here: a trailing \b would have treated hyphen as an
automatic boundary on its own, since hyphen is not a word character, so
a 35 character key ending in a hyphen and glued directly to more text
used to be redacted before the fix in this branch. The mirror assertion
treats that the same as every other "one more alphanumeric character
right there" case already covered, on the same reasoning already applied
to AWS access key. That reasoning is sound, but it was not written down
or tested, so it looked like an accident rather than a choice. Added a
comment and a test pinning it.

94 passed, tests/test_credential_redactor.py.

Signed-off-by: qubeena07 <qubeena7@gmail.com>

* fix(agent-os): redact a Google API key ending in a hyphen when glued to more text

Review on this PR pointed out that a detector change must never lose a
shape the previous version caught. The mirror assertion alone loses one:
a trailing \b treats hyphen as an automatic boundary on its own, since
hyphen is not a word character, so a 35 character key ending in a hyphen
and glued straight to more text was redacted before this branch, and the
mirror assertion by itself does not see that, since it only looks at
what follows, not at what was actually consumed.

The pattern now also accepts whenever the character actually consumed at
that position is a hyphen, on top of the existing "not alphanumeric"
check, restoring the original behavior for that one shape while keeping
the "one more alphanumeric character means a different token" guard for
every other ending. Verified against the previous pattern across a grid
of 35th characters and following characters: the only three cases that
change are exactly the ones this is meant to fix, nothing else widens.

Flips the pin test added for this same gap into a positive regression
test, since the gap is now closed rather than documented as accepted.

94 passed, tests/test_credential_redactor.py. Reverting only the source
change and keeping the flipped test: it fails, confirming the test
exercises this fix.

Signed-off-by: qubeena07 <qubeena7@gmail.com>

---------

Signed-off-by: qubeena07 <qubeena7@gmail.com>
Signed-off-by: yuvrajsingh2428 <offcyuvi2428@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR (< 200 lines) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CredentialRedactor misses a complete secret when a suffix is glued to it (AKIA..._old is not redacted at all)

4 participants