diff --git a/enroll/ignore.py b/enroll/ignore.py index 563a9ee..7919d1b 100644 --- a/enroll/ignore.py +++ b/enroll/ignore.py @@ -180,8 +180,21 @@ HIGH_CONFIDENCE_SECRET_PATTERNS = [ ), # HTTP(S) Authorization / Proxy-Authorization header values carrying a # bearer/basic/digest credential. + # + # This is high-confidence and therefore scanned against the RAW bytes so a + # *commented-out* populated header (a real credential that was merely + # disabled) is still refused in safe mode. For that to work the pattern must + # NOT anchor to the physical start of line with ``^\s*``: a single-line + # comment marker (``#``, ``;``, ``//``, ``*`` block-continuation, ``dnl``, + # ``--``, ``REM`` ...) sits between the line start and ``authorization`` and + # would defeat a ``^\s*`` anchor, letting the header slip past the raw scan + # (the soft/comment-aware tier never sees it, because that tier strips + # comment lines by design). Instead we require the header to be preceded by + # start-of-input or any non-identifier character, which is comment-marker + # agnostic and still avoids matching an identifier such as + # ``my_authorization:``. re.compile( - rb"(?im)^\s*(?:proxy-)?authorization\s*:\s*" + rb"(?im)(?:^|[^A-Za-z0-9_.-])(?:proxy-)?authorization\s*:\s*" rb"(?:bearer|basic|token|digest)\s+\S" ), ] diff --git a/tests/test_secret_detection.py b/tests/test_secret_detection.py index 3c98d64..8de887c 100644 --- a/tests/test_secret_detection.py +++ b/tests/test_secret_detection.py @@ -30,6 +30,46 @@ def test_authorization_headers_denied(): ), data +def test_commented_authorization_headers_denied(): + """A commented-out populated Authorization header is a real (disabled) + credential and must still be refused in safe mode, regardless of the + single-line comment marker used. This is high-confidence material, so it is + scanned against the raw bytes; the pattern must not anchor to the physical + start of line, or a leading comment marker defeats it (the soft/comment-aware + tier never sees it because that tier strips comment lines).""" + + pol = IgnorePolicy(dangerous=False) + for data in [ + b"# Authorization: Bearer eyJhbGciOiJIUzI1NiIs\n", + b"#Authorization: Bearer eyJhbGciOiJIUzI1NiIs\n", + b"; Authorization: Basic dXNlcjpwYXNzd29yZA==\n", + b"// Authorization: Digest abc123\n", + b" //Authorization: Bearer abc123\n", + b"* Authorization: Bearer abc123\n", # block-comment continuation line + b"\t# Authorization: Token abc123\n", + b"dnl Authorization: Bearer abc123\n", # m4-style comment + b"-- Proxy-Authorization: Basic abc123\n", # sql/lua-style comment + b"REM Authorization: Bearer abc123\n", # bat-style comment + b"# Proxy-Authorization: Bearer xyz\n", + ]: + assert ( + pol._content_deny_reason("/etc/svc/app.conf", data) == "sensitive_content" + ), data + + +def test_authorization_header_word_not_false_positive(): + """The Authorization-header pattern must not fire on benign prose mentions, + a non-scheme value, or an identifier that merely ends in 'authorization'.""" + + pol = IgnorePolicy(dangerous=False) + for data in [ + b"# how to set the authorization header is documented elsewhere\n", + b"authorization: required\n", + b"my_authorization: bearer x\n", + ]: + assert pol._content_deny_reason("/etc/svc/app.conf", data) is None, data + + def test_benign_urls_not_false_positive(): pol = IgnorePolicy(dangerous=False) for data in [