Skip to content

fix(tinyredact): redact cards next to other digits and quoted or prefixed credentials - #308

Merged
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/tinyredact-card-and-credential-gaps
Oct 22, 2026
Merged

M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/tinyredact-card-and-credential-gaps

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Summary

Three content-redaction gaps let real credentials through redact_pii* / sanitize_text*, under both card policies.

Card next to other digits. CC_RE is greedy, so card 4111 1111 1111 1111 123 matches all 19 digits. That run fails Luhn, and find_iter never retries a shorter one, so the card survived. A leading number (qty 2 4111 1111 1111 1111) fails the same way. The regex crate has no lookaround to fix this in the pattern.

  • A separated run that fails Luhn as a whole is now retried on windows of its contiguous digit groups, 13–19 digits, longest first, then leftmost.
  • A window must pass Luhn and be corroborated: a real network IIN at an issued length (plausible_card_number) or a card keyword nearby (cc_keyword_near). This applies under either gate, because splitting a run multiplies the chance of a Luhn pass by luck.
  • Bare runs are a single group, so they are never split. The epoch-millisecond protection (opencompany#1201) is unchanged.
  • Both gates go through one card_span helper. The BareCardGate doc now describes the retry.

JSON-quoted credentials in prose. The api_key pattern and the password / secret / token pattern allowed no quote between the key and its separator. As a result, {"password": "hunter2", "api_key":"abcd1234"} inside text was left as is. The OAuth-names pattern had the same gap. All three now accept an optional quote there.

DB_PASSWORD= / *_SECRET= / *_TOKEN=. The leading \b never fires after _, so prefixed environment-style assignments survived. The pattern now starts with (?:\b|_). The trailing \b is kept on purpose: it is what leaves token_count=5, max_tokens: 10, tokenizer=x, password_reset_sent: true and secret_santa=… alone.

Related issue

None. Found in an audit for tinyhumansai/openhuman.

API or behavior changes

No API change. Behaviour only ever adds redactions:

  • A Luhn-valid, corroborated card inside a longer separated digit run is redacted, and the surrounding digits are kept.
  • JSON-quoted and _-prefixed credential assignments are redacted. The prefix match starts at the _, so DB_PASSWORD=x becomes DB[REDACTED]. That is cosmetic; the value is gone.
  • Owner call taken (the audit plan's option): the broader credential pattern also feeds has_likely_secret, which openhuman uses to refuse flow-memory writes. The intended new refusal is {"token":"abc"}. Reading the regex, it will also refuse lookalikes whose value follows a credential word directly, e.g. is_secret: false, has_password: true, max_token=5. Those false positives are accepted rather than weakening the pattern. Making the match depend on the value is the follow-up if they show up in practice.
  • Not included: an arm for secret_access_key (AWS_SECRET_ACCESS_KEY=, STRIPE_SECRET_KEY=), and the fail-closed merge of partially overlapping hits in dedupe_overlaps. Both were left out to keep this change to the three reported gaps.

Validation

  • N/A: cargo fmt --all -- --check was not run; rustfmt --edition 2024 was run on each touched file. CI runs the full check.
  • N/A: cargo clippy --all-targets --all-features -- -D warnings was not run locally; CI runs it.
  • N/A: cargo build --all-targets --all-features was not run locally; CI runs it.
  • N/A: cargo test --all-features was not run locally. Only the six tests below were run, in one targeted invocation; CI runs the full suite.

cargo test -p tinyredact --lib -- <the six tests below>:

  • with pii/mod.rs and sanitize/patterns.rs reset to main (tests kept): FAILED, 2 passed (the two guards), 4 failed:
    • credit_card_followed_by_space_and_digits_redacted: card survived under both policies;
    • credit_card_after_a_leading_number_is_redacted_and_the_number_kept: card survived: "qty 2 4111 1111 1111 1111";
    • sanitize_text_redacts_json_quoted_credentials_in_prose: hunter2 and abcd1234 survived under both policies;
    • sanitize_text_redacts_prefixed_env_style_credentials: all three values survived under both policies.
  • with the fix: ok, 6 passed.

Tests

  • Regressions: the four tests above.
  • Guards:
    • a_separated_digit_list_with_no_card_is_left_alone: ids 123 1000 2000 1000 0002. The whole run fails Luhn, and its only Luhn-valid window has no network IIN and no keyword, so it stays unredacted under both gates.
    • sanitize_text_leaves_credential_lookalike_names_alone: token_count, max_tokens, tokenizer, password_reset_sent and secret_santa are unchanged.
  • Not tested: the flow-refusal false positives above. They are an accepted trade-off, not pinned behaviour.

Documentation

The BareCardGate doc and the card_span doc describe the window retry. No external docs cover these patterns.

Checklist

  • The change is focused on one logical change
  • No new #[allow(...)], #[ignore], or relaxed lints
  • No secrets, tokens, or .env contents in the diff or the description

…ixed credentials

A separated card run that failed Luhn as a whole (a card followed by a
CVV or preceded by a quantity) was dropped, because the greedy card regex
cannot give digits back. Such a run is now retried on windows of its
digit groups; a window must pass Luhn and be corroborated.

The credential patterns allowed no quote between the key and its
separator, so JSON-quoted credentials in prose survived, and their
leading word boundary never fired after an underscore, so DB_PASSWORD=,
*_SECRET= and *_TOKEN= survived. Both now match; the trailing word
boundary still keeps token_count, max_tokens and tokenizer clean.
@coderabbitai

coderabbitai Bot commented Oct 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 5 billable files and costs up to $1.25.

Or wait 26 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a3c221c6-3817-4007-b9b0-560443879e35
📥 Commits

Reviewing files that changed from the base of the PR and between 1be003e and b09bbb0.

📒 Files selected for processing: 5
  • crates/tinyredact/src/pii/mod.rs
  • crates/tinyredact/src/pii/mod_tests.rs
  • crates/tinyredact/src/policy/mod.rs
  • crates/tinyredact/src/sanitize/mod_tests.rs
  • crates/tinyredact/src/sanitize/patterns.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@tinysweeper

tinysweeper Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Reviewed head: b09bbb0f19bb86547d278a9e26b3d8394cfbe406

Verdict: Ready for maintainer review · Priority: none
Coverage: Reviewed 5 of 5 files.
Pull request purpose (title): fix(tinyredact): red…

Diagnostics and supporting details

Full summary

Pull request purpose (title): fix(tinyredact): redact cards next to other digits and quoted or prefixed credentials. Tiny Sweeper completed its review; deterministic results follow.

Review snapshot

Change surface Files Review signal Count
Production 3 Active findings 0
Tests 2 Noted findings 0
Documentation 0 Resolved findings 0
Configuration 0 Pending checks/questions 0

Completeness: Complete
Test assessment: Test coverage is assessed from changed tests and lane evidence; execution is not claimed without trusted check data.

What changed

Card detection now routes every `CC_RE` match through a new `card_span` helper. A run that passes Luhn is still taken as the card (under LuhnOnly, when separated, or when a bare run is corroborated). A separated run that fails Luhn as a whole is retried on windows of its contiguous digit groups, 13-19 digits, longest first then leftmost, and a window must pass Luhn and be corroborated by `plausible_card_number` or `cc_keyword_near` under either gate. Bare runs are a single group and are never split, so the epoch-millisecond protection is unchanged. Credential patterns in `patterns.rs` now accept an optional quote between the key and its separator for `api_key`, the `password`/`secret`/`token` group, and the OAuth-names group, and the broad credential pattern starts with `(?:\b|_)` while keeping the trailing `\b`. The description notes two accepted side effects: `has_likely_secret` will also refuse lookalikes such as `is_secret: false`, and a prefix match starts at the `_`, so `DB_PASSWORD=x` renders as `DB[REDACTED]`. Not included: a `secret_access_key` arm and the fail-closed `dedupe_overlaps` merge.

Features

  • Added — Card window retry inside longer separated digit runs: A card followed or preceded by other digits, such as `card 4111 1111 1111 1111 123` or `qty 2 4111 1111 1111 1111`, is now redacted instead of surviving, while the surrounding digits are kept. Window matches must pass Luhn and be corroborated, limiting chance Luhn passes created by splitting. (crates/tinyredact/src/pii/mod.rs fn push_captured(, crates/tinyredact/src/policy/mod.rs)
  • Modified — Quote-tolerant credential key patterns: An optional quote is now allowed between the key and its separator for `api_key`, the `password`/`secret`/`token` group, and the OAuth-names pattern, so JSON-quoted credentials like `{"password": "hunter2", "api_key":"abcd1234"}` in prose are redacted. The trailing `\b` is kept so lookalike names such as `token_count` and `secret_santa` stay untouched. (crates/tinyredact/src/sanitize/patterns.rs pub(super) static REDACTION_PATTERNS: LazyLock<Vec<(Regex, &'static str)>> = Laz)
  • Modified — `_`-prefixed environment-style credential matching: The broad credential pattern now starts with `(?:\b|_)`, so assignments like `DB_PASSWORD=`, `STRIPE_SECRET=`, and `GITHUB_TOKEN=` are redacted; the match starts at the `_`, which is cosmetic. This pattern also feeds `has_likely_secret`, so the broader matching can refuse additional flow-memory writes, including accepted false positives such as `is_secret: false`. (crates/tinyredact/src/sanitize/patterns.rs pub(super) static REDACTION_PATTERNS: LazyLock<Vec<(Regex, &'static str)>> = Laz)

Tests

  • regression — Asserts a card after a leading number is redacted and the leading number survives.: Covers the second card ordering; same caveat on execution. (crates/tinyredact/src/pii/mod_tests.rs fn prefilter_is_superset_of_legacy_screen() {)
  • guard — Asserts `ids 123 1000 2000 1000 0002` is left unredacted under both policies because the only Luhn-valid window lacks a network IIN and a nearby keyword.: Pins the negative case for the window retry; same caveat on execution. (crates/tinyredact/src/pii/mod_tests.rs fn prefilter_is_superset_of_legacy_screen() {, crates/tinyredact/src/sanitize/mod_tests.rs fn sanitize_text_leaves_bearer_prose_alone() {)
  • regression — Asserts JSON-quoted credentials in prose are redacted under both policies.: Covers the quoted-credential gap; the description states it failed before the fix and passes with it, but execution is not confirmed by trusted check evidence. (crates/tinyredact/src/sanitize/mod_tests.rs fn sanitize_text_leaves_bearer_prose_alone() {)
  • regression — Asserts `DB_PASSWORD`, `STRIPE_SECRET`, and `GITHUB_TOKEN` assignments are redacted under both policies.: Covers the prefixed-assignment gap; same caveat on execution. (crates/tinyredact/src/sanitize/mod_tests.rs fn sanitize_text_leaves_bearer_prose_alone() {)
  • guard — Asserts `token_count`, `max_tokens`, `tokenizer`, `password_reset_sent`, and `secret_santa` are unchanged and the report records no change.: Pins the trailing-`\b` lookalike exemption. The tests lane noted that a `<key>_token=<value>`-style key is not exercised against the new `(?:\b|_)` alternation to confirm the capture-only prefix is unchanged. (crates/tinyredact/src/sanitize/mod_tests.rs fn sanitize_text_leaves_bearer_prose_alone() {)

Findings

No active actionable findings.

Before merge

None.

Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 5 files; 0 findings. _The code index is behind this pull request (indexed at `2eb32fb0d586`), so retrieved context may be out of date._

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 5 files; 0 findings. _The code index is behind this pull request (indexed at `2eb32fb0d586`), so retrieved context may be out of date._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The tests lane found the card-window logic and the tightened credential regexes each covered by tests that pin intended behaviour for both policies, including the negative case, and found no reachable input where the new code misbehaves. It flagged one gap: no test exercises a `<key>_token=<value>`-style key against the new `(?:\b|_)` alternation.
  • Lane summary: The card-window logic in `card_span` and the tightened credential regexes each come with tests that pin the intended behaviour for both policies, including the negative case; I found no reachable input where the new code misbehaves or a stated invariant left unpinned. One gap is worth flagging: the pattern change to the credential-key regex also loosens the `/secret/<key>` marker path's neighbours, and no test exercises a `<key>_token=<value>`-style key against the new `(?:\b|_)` alternation to confirm the capture-only prefix is unchanged. _The code index is behind this pull request (indexed at `2eb32fb0d586`), so retrieved context may be out of date._

commits

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The commits lane reported no sensitive content in committed files.
  • Lane summary: No sensitive content in committed files. _The code index is behind this pull request (indexed at `2eb32fb0d586`), so retrieved context may be out of date._

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The description matches the diff: it names all three redaction gaps (card runs adjacent to other digits, JSON-quoted credentials, `_`-prefixed env-style assignments), describes the `card_span` window retry and the `(?:\b|_)` prefix change, and its stated behaviour changes, guards, and untested trade-offs line up with the code and tests added here. I found no defect worth reporting. _The code index is behind this pull request (indexed at `2eb32fb0d586`), so retrieved context may be out of date._
Evidence and run details
  • Models: ladder/vectors-oai3, deepseek-v4.1-flash
  • Spend: $0.000931
  • Tokens: 426398 input · 6424 output · 72192 cached · 435 embedding
  • Updated: 2026-10-22T21:46:17Z
Head State Pass summary
b09bbb0f19bb ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-10-22T21:46:17Z)

tinysweeper 0.1.2

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tinysweeper found nothing blocking. Approving.

Diagnostics

Total spend: $0.0009

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Oct 22, 2026
@M3gA-Mind
M3gA-Mind merged commit 09e3741 into tinyhumansai:main Oct 22, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant