Coverage for astrocyte/_log_safety.py: 100%

6 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-04 05:24 +0000

1"""Log-injection sanitization helpers. 

2 

3CodeQL's ``py/log-injection`` query flags any log statement that 

4substitutes a user-controlled value, because newline / carriage-return 

5characters in the value can forge fake log entries (CWE-117). 

6 

7The fix is to strip CRLF (and ASCII control characters more broadly) 

8from user-controlled values before they reach the formatter. We do this 

9with a tiny ``safe()`` helper rather than reaching for a structured 

10logger — Astrocyte's existing log call sites are scattered and we want 

11the smallest possible change at each one: 

12 

13 logger.info("retain bank=%s", safe(bank_id)) 

14 

15``safe`` accepts any value (str, int, UUID, dict, ...). For strings, it 

16replaces every control character (0x00-0x1F, 0x7F) with a single space. 

17For non-strings it falls back to ``str(value)`` first, then sanitizes 

18the result. This keeps surrounding log context intact while preventing 

19log forgery and terminal-escape injection. 

20 

21The double-pipeline (``str(value)`` then sanitize) means an attacker 

22controlling a ``__str__`` method on a custom class cannot bypass the 

23filter by returning bytes with embedded newlines. 

24""" 

25 

26from __future__ import annotations 

27 

28from typing import Any 

29 

30_CONTROL_TRANSLATE = {c: 0x20 for c in range(0x20)} 

31_CONTROL_TRANSLATE[0x7F] = 0x20 

32 

33 

34def safe(value: Any) -> str: 

35 """Return a log-safe string representation of ``value``. 

36 

37 Replaces ASCII control characters (``\\x00``-``\\x1F`` and ``\\x7F``) 

38 with a single space. Intended for substitution into log-format 

39 arguments where the value originates from user input — request 

40 bodies, metadata dicts, query parameters, etc. 

41 

42 >>> safe("hello\\nworld") 

43 'hello world' 

44 >>> safe("bank-42") 

45 'bank-42' 

46 >>> safe(None) 

47 'None' 

48 """ 

49 return str(value).translate(_CONTROL_TRANSLATE)