Feb 24, 2026·6 min read·33 visits
loggingredactor < 0.0.6 converts ALL log arguments to strings to check for secrets. This breaks `logger.info('%d', 123)` because `%d` expects an int, not the string '123'. Result: App crashes and lost logs.
In the world of Python logging, laziness is a virtue—until a library like loggingredactor comes along and forces everyone to work too hard. CVE-2026-22041 (GHSA-rvjx-cfjh-5mc9) exposes a fundamental misunderstanding of Python's logging architecture within the loggingredactor library (versions < 0.0.6). By aggressively casting all non-collection types to strings in an attempt to sanitize data, the library broke the contract of lazy string formatting. This resulted in application crashes via `TypeError` whenever a developer tried to log a number using numeric format specifiers. While not a remote code execution flaw, it represents a significant 'Denial of Observability' and stability risk, effectively blinding operations teams when they need logs the most.
We all know the drill. You're building a Python application, and your security team (or your paranoia) whispers in your ear: "Don't log PII. Don't log API keys." So, you grab a library like loggingredactor. Its job is simple: intercept log records, scrub the nasty bits using regex or dictionary keys, and pass the clean data along. It's the janitor of your application logs.
But here is the problem with overzealous janitors: sometimes they throw out the furniture while trying to sweep the floor. loggingredactor made a critical architectural error. It assumed that everything passed to a logger is destined to be a string immediately. It forgot that Python logging is designed to be lazy.
When you call logger.info("User ID: %d", user_id), Python doesn't format that string right away. It passes the format string and the integer user_id to the logging handlers. This optimization saves CPU cycles if the log level is set to WARNING, meaning the INFO log never actually gets formatted. loggingredactor inserted itself into this pipeline and decided to forcefully convert user_id to a string ("123") before the formatter got to it. When the formatter finally woke up and saw %d paired with "123", it threw a tantrum—specifically, a TypeError.
The root cause here is a classic case of "Hammer, meet Nail." The developers of loggingredactor needed to run regex substitutions on log data. Regex works on strings. Therefore, the logic went: "Make everything a string."
In versions prior to 0.0.6, the RedactingFilter.redact method contained a recursive loop. If it encountered a dictionary, it dove in. If it encountered a list, it iterated. But for everything else—integers, floats, booleans, custom objects—it executed this ruthless line of code:
content_copy = isinstance(content_copy, str) and content_copy or str(content_copy)This looks like a clever one-liner, but it's a landmine. It forces a type coercion. If you passed 123 (int), it became '123' (str). If you passed True (bool), it became 'True' (str).
The crash happens downstream in the standard library's logging module. When the LogRecord is processed, the formatting operator % is applied. If the message template is "Value: %d" and the argument has been mutated into a string, Python raises TypeError: %d format: a number is required, not str.
This essentially turns your logging infrastructure into a minefield. Any developer using type-specific formatters (%d, %f, %x) will inadvertently crash the application thread just by trying to write a log message.
Let's look at the "before" and "after" code to understand exactly how this was remediated. This diff is from the critical patch in version 0.0.6.
The Vulnerable Code (v0.0.5):
# Inside RedactingFilter.redact(self, msg)
if isinstance(content_copy, dict):
# ... handle dicts ...
else:
# THE BUG: Force everything to string so we can regex it
content_copy = isinstance(content_copy, str) and content_copy or str(content_copy)
for pattern in self._mask_patterns:
content_copy = re.sub(pattern, self._mask, content_copy)It's that else block that causes the pain. It assumes that if it's not a dict, it must be coercible to a string for regex processing.
The Fixed Code (v0.0.6):
from collections.abc import Mapping
# Inside RedactingFilter.redact(self, msg)
if isinstance(content_copy, Mapping):
# ... handle dicts safely ...
elif isinstance(content_copy, str):
# ONLY apply regex if it is ALREADY a string
for pattern in self._mask_patterns:
content_copy = re.sub(pattern, self._mask, content_copy)
# Note: If it's not a Mapping or a str, it implicitly does nothing and returns the original object.The fix is subtle but vital. It stops the implicit casting. If content_copy is an integer, it hits the end of the if/elif chain and remains an integer. The logging formatter downstream stays happy because %d receives an integer. However, as we'll discuss later, this fix introduces a new, interesting behavior regarding what actually gets redacted.
Exploiting this isn't about popping a shell; it's about causing chaos in the application's stability and observability. Imagine a scenario where a financial application processes transactions. It logs the transaction ID and the amount using standard logging practices.
import logging
from loggingredactor import RedactingFilter
# Vulnerable setup
logger = logging.getLogger('banking_app')
logger.addFilter(RedactingFilter())
# The harmless developer writes this:
tx_id = 94821
amount = 500.00
# The crash:
# The filter turns tx_id into "94821".
# The formatter expects an int for %d.
logger.info("Processing TX ID: %d", tx_id)When this code runs, the application raises an unhandled TypeError. If this logging call is inside the main transaction loop and not wrapped in a specific try/except block for logging errors (which nobody writes), the entire transaction fails.
> [!NOTE] > The Irony: The crash often happens inside the error handling routine.
Consider an app that catches an exception, tries to log it, and then crashes while logging the error. You lose the original error trace and are left with a TypeError from the logging library. This effectively blinds the ops team to the real root cause of production issues.
The patch in version 0.0.6 solves the crash by removing the forced string conversion. However, this introduces a fascinating side-effect that I like to call the "Redaction Bypass via Type confusion."
In the old version (v0.0.5), the library was paranoid. It converted everything to a string and checked it against regex patterns. If you had a credit card number stored as an integer (bad practice, but it happens) or a float, the old version would stringify it and redact it.
The New Behavior (v0.0.6):
# Since we only check `isinstance(x, str)`:
secret_pin = 1234
# Regex pattern is r'\d{4}'
# This integer bypasses the redaction logic entirely because it is not a string!
logger.info("User PIN: %s", secret_pin)Because secret_pin is an integer, the new redact method ignores it. The logging formatter eventually converts it to a string for output, after the redaction filter has already finished its job. The result? Cleartext secrets in logs.
This is a classic trade-off. The maintainers chose application stability (stop crashing on %d) over aggressive, paranoid redaction. It is now the developer's responsibility to ensure that sensitive fields are cast to strings before logging if they rely on regex-based redaction.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
loggingredactor armurox | < 0.0.6 | 0.0.6 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-704 (Incorrect Type Conversion) |
| CVSS v3.1 | 5.3 (Medium) |
| Attack Vector | Local (via Log Arguments) |
| Impact | Denial of Service (App Crash) / Data Loss |
| EPSS Score | 0.00042 |
| Exploit Maturity | PoC Available |
Incorrect Type Conversion or Cast
CVE-2026-115923 (GHSA-w658-mh9p-ww8f) is a path traversal and access control bypass vulnerability in Spotify's Backstage developer portal framework, specifically within the @backstage/plugin-catalog-backend package. Prior to patched versions, DefaultCatalogRulesEnforcer evaluated user-supplied URL catalog locations against configured catalog.rules patterns using raw, uncanonicalized strings. Because downstream URL readers canonicalize URLs and resolve dot segments before fetching resources, an authenticated attacker with location registration permissions can supply percent-encoded traversal sequences (such as %2e%2e) to bypass location restrictions and register untrusted catalog entities.
CVE-2026-116576 (GHSA-3f7j-7jwc-fxfj) is an OS Command Injection vulnerability (CWE-78) in the Node.js systeminformation library prior to version 5.33.16 on Microsoft Windows. The services() API function dynamically constructs a PowerShell Get-CimInstance command using caller-supplied service names passed through sanitizeShellString(). While the sanitizer strips standard ASCII quotation marks, it permits Unicode typographic quotation marks (U+2018 through U+201E). Because the Windows PowerShell tokenizer recognizes Unicode quotation marks as valid string literal delimiters, an attacker who controls the service name input can break out of the CIM filter string and execute arbitrary PowerShell commands within the security context of the Node.js process.
An interpretation conflict vulnerability (CWE-436) exists in the Ruby CSS parser library Crass prior to version 1.0.8. When consuming the remnants of a malformed URL token, the tokenizer erroneously checks the previously consumed character to determine if a valid escape sequence is present. Consequently, a closing parenthesis immediately following an escaped backslash sequence is treated as escaped by Crass, while spec-compliant web browsers treat the closing parenthesis as the end of the bad URL token. Downstream CSS sanitizers such as Sanitize, Loofah, and rails-html-sanitizer that rely on Crass to parse and reserialize untrusted CSS can be bypassed to inject arbitrary CSS declarations, style rules, and external @import directives.
Tornado versions prior to 6.6.0 contain an inefficient algorithmic complexity vulnerability in the HTTP header parser. When processing requests with obsolete line folding (obs-fold), the HTTPHeaders.parse_line method repeatedly concatenates immutable Python strings within a list structure. An unauthenticated remote attacker can send an HTTP request containing thousands of short header continuation lines to trigger quadratic O(N^2) CPU consumption on the main IOLoop thread, starving concurrent connections and resulting in a denial of service.
Tornado versions prior to 6.6.0 contain an open redirect vulnerability (CWE-601) in tornado.web.RedirectHandler. When route templates incorporate regular expression capture groups at the start of a path, directly after a fixed host without a trailing slash, or at the beginning of a destination template, unvalidated URL-decoded parameters allow attackers to manipulate the redirect scheme or network location. Because modern browsers normalize URLs according to the WHATWG specification—treating backslashes as forward slashes and stripping control characters—attackers can bypass naive prefix checks and redirect victims to arbitrary external domains.
Tornado versions prior to 6.6.0 contain an uncontrolled resource consumption vulnerability (CWE-400) in the HTTP/1.1 chunked transfer encoding parser. When processing incoming chunked payloads that have already buffered in the socket read buffer, Tornado parses chunks in a tight synchronous loop without yielding to the event loop. Furthermore, non-streaming request handlers and SimpleAsyncHTTPClient store each chunk as a discrete Python bytes object inside a list, inducing an approximately 20-fold memory overhead for 1-byte chunks.