BUG-HUNT: [security] mask_database_url() partial masking when password contains @ — credential fragment leaks in masked URL #6611

Open
opened 2026-04-09 22:16:44 +00:00 by HAL9000 · 0 comments
Owner

Bug Report: Security — mask_database_url() Partial Masking with @ in Password

Severity Assessment

  • Impact: Database password fragments leak into masked URLs that are written to structured logs. Any password containing @ (e.g., p@ss, pass@word) is only partially masked — the portion after the first @ remains visible.
  • Likelihood: Uncommon but realistic. Passwords with @ signs are legal in database URLs and are occasionally required by cloud-managed databases or password managers. The masking function is called in production log paths.
  • Priority: High — security — credential leakage in log output

Location

  • File: src/cleveragents/shared/redaction.py
  • Function: mask_database_url
  • Lines: 210–215

Description

mask_database_url() uses the regex (://[^:]+:)([^@]+)(@) to detect and replace a user:password@ credential block in a database URL. The pattern [^@]+ matches characters up to (and stopping at) the first @ sign it encounters. When the password itself contains an @ sign, only the password segment before that first @ is replaced with ***. The remainder of the password (after the first @) is left in the masked URL and leaks into logs.

Evidence

# src/cleveragents/shared/redaction.py, lines 210–215
masked = re.sub(
    r"(://[^:]+:)([^@]+)(@)",
    r"\1***\3",
    url,
)

Demonstrable reproduction:

>>> mask_database_url("postgresql://admin:p@ss@word@host/db")
'postgresql://admin:***@ss@word@host/db'
#                         ^^^^^^^ password fragment STILL VISIBLE
>>> mask_database_url("mysql+pymysql://user:pass@123@db.host:3306/app")
'mysql+pymysql://user:***@123@db.host:3306/app'
#                         ^^^^ password fragment STILL VISIBLE

Normal passwords (no embedded @) are correctly masked:

>>> mask_database_url("postgresql://user:normalpassword@localhost/db")
'postgresql://user:***@localhost/db'  # CORRECT

Expected Behavior

mask_database_url("postgresql://admin:p@ss@word@host/db") should return 'postgresql://admin:***@host/db' — all password bytes masked, only host/path retained.

Actual Behavior

Returns 'postgresql://admin:***@ss@word@host/db' — the fragment of the password after the first embedded @ remains visible.

Where This URL Ends Up in Production Logs

mask_database_url is called in two places in production code:

# src/cleveragents/resource/handlers/database.py, line 414
masked = mask_database_url(str(conn_str))

# src/cleveragents/resource/handlers/database.py, line 562
safe_location = mask_database_url(location) if "://" in location else location

Both results are logged via structlog. A masked URL containing the leaked password fragment will appear in any log output (file, console, audit event).

Suggested Fix

Use re.sub with a pattern that matches up to the last @ before the host (or equivalently, replace the entire user:password@ block in one shot using a greedy match on the password):

# Greedy match: replace everything between :// and the LAST @ (the host separator)
masked = re.sub(
    r"(://[^@]*:)([^@]*)(@[^@]+$)",  # captures host part (last @...) separately
    r"\1***\3",
    url,
)

Or use urllib.parse for robustness:

from urllib.parse import urlparse, urlunparse

def mask_database_url(url: str) -> str:
    if not url or url.startswith("sqlite"):
        return url
    try:
        parsed = urlparse(url)
        if parsed.password:
            masked_netloc = parsed.netloc.replace(
                f":{parsed.password}@", ":***@", 1
            )
            return urlunparse(parsed._replace(netloc=masked_netloc))
    except Exception:
        pass
    return url

Category

security

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: Security — `mask_database_url()` Partial Masking with `@` in Password ### Severity Assessment - **Impact**: Database password fragments leak into masked URLs that are written to structured logs. Any password containing `@` (e.g., `p@ss`, `pass@word`) is only partially masked — the portion after the first `@` remains visible. - **Likelihood**: Uncommon but realistic. Passwords with `@` signs are legal in database URLs and are occasionally required by cloud-managed databases or password managers. The masking function is called in production log paths. - **Priority**: High — security — credential leakage in log output ### Location - **File**: `src/cleveragents/shared/redaction.py` - **Function**: `mask_database_url` - **Lines**: 210–215 ### Description `mask_database_url()` uses the regex `(://[^:]+:)([^@]+)(@)` to detect and replace a `user:password@` credential block in a database URL. The pattern `[^@]+` matches characters up to (and stopping at) the **first `@` sign** it encounters. When the password itself contains an `@` sign, only the password segment before that first `@` is replaced with `***`. The remainder of the password (after the first `@`) is left in the masked URL and leaks into logs. ### Evidence ```python # src/cleveragents/shared/redaction.py, lines 210–215 masked = re.sub( r"(://[^:]+:)([^@]+)(@)", r"\1***\3", url, ) ``` Demonstrable reproduction: ```python >>> mask_database_url("postgresql://admin:p@ss@word@host/db") 'postgresql://admin:***@ss@word@host/db' # ^^^^^^^ password fragment STILL VISIBLE ``` ```python >>> mask_database_url("mysql+pymysql://user:pass@123@db.host:3306/app") 'mysql+pymysql://user:***@123@db.host:3306/app' # ^^^^ password fragment STILL VISIBLE ``` Normal passwords (no embedded `@`) are correctly masked: ```python >>> mask_database_url("postgresql://user:normalpassword@localhost/db") 'postgresql://user:***@localhost/db' # CORRECT ``` ### Expected Behavior `mask_database_url("postgresql://admin:p@ss@word@host/db")` should return `'postgresql://admin:***@host/db'` — all password bytes masked, only host/path retained. ### Actual Behavior Returns `'postgresql://admin:***@ss@word@host/db'` — the fragment of the password after the first embedded `@` remains visible. ### Where This URL Ends Up in Production Logs `mask_database_url` is called in two places in production code: ```python # src/cleveragents/resource/handlers/database.py, line 414 masked = mask_database_url(str(conn_str)) # src/cleveragents/resource/handlers/database.py, line 562 safe_location = mask_database_url(location) if "://" in location else location ``` Both results are logged via structlog. A masked URL containing the leaked password fragment will appear in any log output (file, console, audit event). ### Suggested Fix Use `re.sub` with a pattern that matches up to the **last** `@` before the host (or equivalently, replace the entire `user:password@` block in one shot using a greedy match on the password): ```python # Greedy match: replace everything between :// and the LAST @ (the host separator) masked = re.sub( r"(://[^@]*:)([^@]*)(@[^@]+$)", # captures host part (last @...) separately r"\1***\3", url, ) ``` Or use `urllib.parse` for robustness: ```python from urllib.parse import urlparse, urlunparse def mask_database_url(url: str) -> str: if not url or url.startswith("sqlite"): return url try: parsed = urlparse(url) if parsed.password: masked_netloc = parsed.netloc.replace( f":{parsed.password}@", ":***@", 1 ) return urlunparse(parsed._replace(netloc=masked_netloc)) except Exception: pass return url ``` ### Category `security` ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-09 22:25:26 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#6611
No description provided.