Skip to content

Commit a9e5624

Browse files
committed
fix(redaction): treat a prefixed quote as a literal opener
Prefixed multiline assignments such as r""" were whole-masked on the first line, which destroyed the quotes the literal pass needs. Later lines stayed marked in-literal, so inner secrets survived on the snippet.
1 parent 55bc48c commit a9e5624

2 files changed

Lines changed: 32 additions & 6 deletions

File tree

socket_basics/core/utils/redaction.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ def mask_value(value: Any, reveal: int = _DEFAULT_REVEAL,
146146
# such as ``# see get_secret()`` disable masking for the value in front of it.
147147
_CALL_EXPRESSION = re.compile(r'^[\w.\[\]]+\s*\(')
148148

149+
# Optional prefixes on a quoted literal. ``r"""`` / ``f"..."`` / ``b'...'``
150+
# still open a value the literal pass can find; the quote, not the prefix,
151+
# is what that pass matches.
152+
_STRING_PREFIX = re.compile(r'^[bBfFrRuU]*')
153+
149154
# Rule-name fragments whose finding *is* the credential. ``hardcoded-ip`` and
150155
# the password-policy rules deliberately do not appear: their snippets are
151156
# logic, and masking them would remove the reason the finding was raised.
@@ -252,8 +257,16 @@ def _mask_statement(code: str, offset: int, in_literal) -> str:
252257
# nothing to act on. A quoted value is the literal pass's job -- but only
253258
# where the quote opens a literal that pass can find. A snippet cut mid
254259
# string has an opening quote and no closing one, so nothing matches and
255-
# the value would survive untouched.
256-
opens_literal = value.startswith(('"', "'", '`')) and in_literal(offset + len(head))
260+
# the value would survive untouched. A prefix such as ``r`` or ``f`` is
261+
# not the quote, but the quote after it still is: starring the opener
262+
# would drop those quotes, later lines would stay marked in-literal, and
263+
# the body would survive the literal pass.
264+
prefix_len = _STRING_PREFIX.match(value).end()
265+
opens_literal = (
266+
prefix_len < len(value)
267+
and value[prefix_len] in '"\'`'
268+
and in_literal(offset + len(head) + prefix_len)
269+
)
257270
if opens_literal or _CALL_EXPRESSION.match(value):
258271
return code
259272

@@ -292,10 +305,10 @@ def redact_literals(text: Any) -> str:
292305
finding actionable, while the value does not.
293306
294307
Where the shape of the value is not recognized the whole value is masked
295-
rather than guessed at, so a subscript, a ternary or a prefixed literal
296-
(``f"..."``, ``r'...'``) loses more of the line than a plain assignment
297-
does. That direction is deliberate: the rule ID, file and line still
298-
identify the finding, and the alternative is leaving a credential in place.
308+
rather than guessed at, so a subscript or a ternary loses more of the
309+
line than a plain assignment does. That direction is deliberate: the
310+
rule ID, file and line still identify the finding, and the alternative
311+
is leaving a credential in place.
299312
"""
300313
if not isinstance(text, str) or not text:
301314
return text if isinstance(text, str) else ''

tests/test_secret_redaction.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,19 @@ def test_a_literal_spanning_lines_keeps_the_line_structure(self):
309309
assert redacted.count("\n") == 2
310310
assert "hunter2" not in redacted
311311

312+
def test_a_prefixed_multiline_literal_is_masked(self):
313+
# The prefix is not a quote, so whole-masking the first line would
314+
# replace the opener with stars. Later lines stay marked in-literal,
315+
# assignment and comment masking skip them, and the body survives.
316+
for prefix in ("r", "f", "b", "rf", "fr", "br", "R", "rb"):
317+
for quote in ('"', "'"):
318+
snippet = (
319+
f"PASSWORD = {prefix}" + quote * 3
320+
+ "\npassword=hunter2\n# hunter2\n"
321+
+ quote * 3
322+
)
323+
assert "hunter2" not in redact_literals(snippet), snippet
324+
312325

313326
class TestCredentialRuleSelection:
314327
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)