Repository navigation
fix(redaction): pin and passcode are whole-token credentials, scheme-less userinfo is masked, and redact_dict composes redact_nested with no partial reveal (#18194) - #18228
Conversation
…er both policies (#18194) Owner decision on #18194: add pin and passcode to the redaction vocabulary, matched as whole words. A new WHOLE_TOKEN_NOUNS tuple (not BROAD_FRAGMENTS, which is substring-matched and feeds credential_redaction.SENSITIVE_KEYS) is consulted by PRECISE and BROAD alike. Names are tokenised on _ - . and camelCase; the name matches when it ends in pin, pin code or passcode. Masked: pin, user_pin, pinCode, card_pin, passcode. Unmasked: pin_memory, pinned_revision, pin_length, spinner, opinion, pinyin. secret_redaction.py was 599/600 lines, so the vocabulary and matcher (CREDENTIAL_SUFFIXES, AUTHORIZATION_TERMS, BROAD_*, quantity rule, MatchPolicy, matcher) move to secret_redaction_vocabulary.py. secret_redaction re-exports every public name; the census guard treats the pair as one canonical home. Corpus delta, measured before (parent commit) vs after over autobot_shared/redaction_corpus_17336.txt plus the 11 appended contrast names, value int / digit string / True, nothing removed anywhere: - The 3,373 pre-existing names: 0 newly masked under either policy (no name there contains the token pin or passcode). - Appended names newly masked, PRECISE (redact_value, RedactedReprMixin, URL query, portability export): pin, user_pin, pinCode, card_pin, passcode. - Appended names newly masked, BROAD (redact_mapping, redact_dict, cot_events, config_revision, redact_nested): pin, user_pin, pinCode, card_pin. passcode was already masked under BROAD (substring pass).
…ost[:port] value (#18206) The canonical redactor only scrubbed userinfo when a value carried scheme://. A bare user:pw@host:5432 (DSN, proxy or endpoint field) passed through. redact_schemeless_userinfo masks the password of a WHOLE value of that shape to user:**********@host:5432 and is applied by redact_value and redact_nested (any depth, any name, over a URL-shaped name too). No false positives, each pinned by a contrast test: name@host.com (no colon), host:port (no @), mailto: style addresses, prose with whitespace, and scheme URLs (still handled by the URL path). Not applied in redact_text/redact_content: free text has no value boundary, so note:bob@example.com would read as userinfo; the boundary doc says so. Masked set per caller: unchanged for every name (the change is value-side and only widens), asserted over the full corpus.
…_nested, with a fixed-width mask (#18205) redact_nested is now the one recursive mapping redactor. redact_dict is a thin composition over it: - exempt_counts becomes an explicit keyword on redact_nested (default True, unchanged for GET /config and /current); redact_dict passes False because this layer always masked a count of a credential noun (max_tokens: 4096), and no name may leave a caller's masked set. - extra_scan is a new optional hook run over every string left unmasked; redact_dict passes redact_string so the API_KEY_PATTERNS content scan (incl. the 32+ char generic key) that replay_service, safe_repr and the log filter rely on is kept. - The partial reveal value[:4]...value[-4:] (and the *** for other types) is replaced by the fixed-width REDACTED_PLACEHOLDER: eight characters of a key are a disclosure. redact_api_key keeps the partial form only for redact_string's pattern matches, reported on the issue. - _redact_list is gone (dead after the composition). Behaviour changes, both widening or neutral: tuples/sets are now walked; None or "" under a credential name is returned as-is instead of ***. Proof the masked set does not shrink: test_no_name_leaves_redact_dicts_masked_set runs the frozen main implementation against the new one over the whole 3,384-name corpus for int, float, digit string, JWT, True and an sk- string: zero names unmasked. The #17336 policy test's redact_dict caller entry (BROAD, no count exemption) is unchanged and still green.
…o no llm_shared path reveals part of a key (#18205) redact_string's API_KEY_PATTERNS matches (CredentialRedactionFilter, safe_repr) still showed value[:4]...value[-4:]. #18205 asks for the partial reveal to be justified or replaced; nothing justifies it, so it is replaced. THREAT_MODEL anchors follow the vocabulary split.
… under both policies; gpio_pin consequence recorded (#18194)
… # in the password and an IPv6 host, spares a drive path (#18206)
…act_nested scrubs only plain endpoint names, scalars pass through, no-shrink test pins base as data (#18205)
…se, and re-point THREAT_MODEL anchors (#18205) Rebasing onto #18219's 0037917 replayed _redact_string_leaf with mask_username=cred; #18219 masks ALL URL userinfo (a key can sit in the user slot). Restored here, with the docstring to match. The redact_text/redact_mapping anchors follow the vocabulary split.
… on main, redact_nested joins the per-caller no-shrink test (#18205)
…d and spinners are not (#18194)
…15 lines instead of 19,640 (#18205) Same 19,635 (name, value) pairs, now `<name><TAB><value ids>`. Three of the seven values (jwt, sk, userinfo) are masked under every corpus name, so the pair-per-line form was mostly repetition. Mutation re-adding "_endpoint" to the exemption still fails the urlpath case.
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 41 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
|
✅ SSOT Configuration Compliance: Passing🎉 No new hardcoded values of either class — Known backlog in |
…ule_names, not a hand-kept set (#18205) The hand-kept _STDLIB lacked decimal, so the first module on the agent's import path to use it read as an uninstalled third-party package. The interpreter's own list replaces it; a contrast pair pins that decimal/asyncio need no install and yaml/httpx do.
Thinking Path
Three open redaction defects all live in
autobot_shared/secret_redaction.pyand its one second recursive redactor,llm_shared/credential_redaction.redact_dict, so they ship as one batch (same file ⇒ one PR). The owner decided #18194 on 2026-10-10:pinandpasscodeare credentials when they appear as whole words. #18205 asks for one recursive mapping redactor and an end to the partial key reveal. #18206 asks for the password in a scheme-lessuser:password@hostto be masked. It builds onredact_nestedfrom #18219 (merged).What Changed
autobot_shared/secret_redaction_vocabulary.py.secret_redaction.pyre-exports every public name, so no import breaks (594/600 lines, no ratchet entry).redaction_concept_census_testnames both files as the one canonical home (CANONICAL_HOMES), and a third file still fails it.pin,pins,passcode,pincode(s),pin_number(s),pin_codesandpinfollowed byhash/digest/saltare matched as whole tokens (split on_ - .and camelCase).pin_memory,pinned_revision,pin_length,spinner,opinionandpinyinstay unmasked. They are not added to the substring-matchedBROAD_FRAGMENTS. Consequence of the ruling, kept deliberately (confirmed via the coordinator):gpio_pin,led_pinandversion_pinare masked. It is recorded inREDACTION_BOUNDARY.mdand pinned by a test.redact_schemeless_userinfomasks the password inuser:password@host[:port][/…]. It splits on the last@and accepts/,#and:in the password and a bracketed IPv6 host.redact_valueandredact_nestedapply it. Unchanged: emails (including+),host:port,mailto:/sip:/tel:, and a drive pathC:\x@y. It is deliberately not applied to free text, which has no value boundary (note:bob@example.com).redact_dictis nowredact_nested(data, BROAD, exempt_counts=False, extra_scan=redact_string, mask_credential_urls=True). Effects:webhook_secret_urlcan never come back.redact_nested's own default masks a credential-named URL whole too, except plain endpoint URL names (token_url,auth_url,authorization_url,db_dsn), which keep their host as on main.*_endpointcredential names (oauth_token_endpoint,token_endpoint) are masked whole, as on main, and so are dash-spelled names (db-dsn). Unknown name parts fail closed.redact_dictandredact_api_key(behindredact_string,safe_reprandCredentialRedactionFilter) return the fixed-widthREDACTED_PLACEHOLDERinstead ofvalue[:4]...value[-4:]. The three other partial-reveal sites outside this module are one class issue, security(redaction): three key maskers outside the canonical redactor still reveal part of the key #18221.redact_dictcallers (also inREDACTION_BOUNDARY.md):None/""under a credential name is returned as-is;**********;bytesfails closed.redact_dictcallers, before and after (call sites unchanged):llc/services/replay_service._redact_sensitiveredact_string)llc/services/replay_service._redact_log_dictcredential_redaction.safe_reprredact_dictcredential_redaction.CredentialRedactionFilterredact_dict+redact_stringonrecord.args#18206 inventory of fields that could hold a
user:pass@hostvalue, recorded inREDACTION_BOUNDARY.mdwith file:line. All are documented inscheme://form, and the parsers reject the scheme-less form (SQLAlchemymake_urlraises ArgumentError,redis.Redis.from_urlraises ValueError). No proxy-credential setting was found:ssot_config:database_url,redis_url,custom_endpoint,ollama_gpu_endpoint,jaeger_endpoint;config:database_url,slm_users_database_url,autobot_users_database_url,redis_url;.env.example:AUTOBOT_REDIS_URL,CELERY_BROKER_URL;*_DATABASE_URL,AUTOBOT_REDIS_URL.Verification
pin,user_pin,pinCode,card_pin,passcode,pin_hash,pincode, …;passcode(already masked bypass).redact_dictno-shrink: base f0c9745's realredact_dictmasked-name set is pinned as data (redact_dict_base_masked_names_f0c9745f32.txt, 2439 names). The test asserts base ⊆ new across int, float, digit string, JWT,True, ansk-string, a webhook URL and an endpoint URL.mask_credential_urls=Truefails 5 tests.redact_nestedno-shrink: main c5279ee's realredact_nestedmasked set is pinned as data (redact_nested_base_masked_names_c5279ee366.txt, one line per name: 19,635 (name, value) pairs over 7 values, including a URL with a secret in its path and a URL with userinfo). The test asserts base ⊆ new._endpointto the exemption fails theurlpathcase.db-dsn/token-urldash normalisation), fixed.12:30@sitebecomes12:**********@site, by an explicit test._endpointshrink, a guard(api): a response model with no declared fields and extra:allow passes the secret-field guard while permitting any field #17899 test lost in the rebase (restored verbatim), and the missing security(redaction): scheme-less user:password@host strings are not scrubbed by the canonical redactor #18206 inventory. The first found:webhook_secret_urlunmasked;pin_hash, a no-shrink test blind to URLs;C:\x@y, the gpio_pin documentation.redact_dict({'webhook_secret_url': …, 'token_url': …})masks both.user:p@ss@hostgivesuser:**********@host, andC:\x@y,bob@example.comandhost:5432are unchanged.autobot_shared/*redact*test*.py,redact_url_credentials_test.py, the census, threat-model anchors,credential_field_name_only_17336,symbol_fork_ratchet_17312andno_secret_in_response_model_17865give 499 passed. Backendllm_config_redaction_18193_test,llm_shared/tests/test_credential_redaction.pyandllc/tests/test_replay*.pygive 107 passed.Risks
**********where they showedsk-1...cdef, and hardware/version*_pinnames are masked.word:x@hostis masked. This is documented.Model Used
Anthropic Claude Opus 5.5 (claude-opus-5-5), coordinating and gate review. Anthropic Claude Sonnet 5.5 (claude-sonnet-5-5) subagents for the survey and implementation.
Issue Link
Closes #18194
Closes #18205
Closes #18206
Related: #18219 (merged base), #18221 (partial reveal outside this module), #16941 (
redact_string's 32+ character pattern, unchanged here).Changelog fragment