BUG-HUNT: security - mask_database_url partial mask when password contains literal @ character #7761

Open
opened 2026-04-12 03:26:32 +00:00 by HAL9000 · 3 comments
Owner

Bug Report: Security — mask_database_url Partial Mask for Passwords Containing @

Severity Assessment

  • Impact: Database credentials with @ in the password are only partially masked. The portion of the password after the @ is exposed in the logged/displayed URL, leaking sensitive credential data. The output URL is also structurally malformed.
  • Likelihood: Medium — passwords with @ are valid and common (e.g., generated passwords, URL-encoded chars decoded too early).
  • Priority: High

Location

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

Description

The masking regex uses [^@]+ to match the password field, which stops at the first @ character. If a database password legitimately contains an @ (which is valid per RFC 3986 when percent-encoded, or via direct inclusion in some drivers), only the portion before the first @ is masked. The rest of the password appears verbatim after the substitution.

Evidence

# redaction.py lines 210–214
masked = re.sub(
    r"(://[^:]+:)([^@]+)(@)",  # [^@]+ stops at FIRST @ in password!
    r"\1***\3",
    url,
)

Demonstration:

url = "postgresql://user:pass@word@localhost/db"
result = mask_database_url(url)
# result == "postgresql://user:***@word@localhost/db"
# BUG: "word" portion of password "pass@word" is exposed!

Expected output: the full password should be replaced, yielding postgresql://user:***@localhost/db.

Expected Behavior

The entire password portion (everything between : after the username and the @ before the hostname) is replaced with ***, regardless of whether the password contains @ characters.

Actual Behavior

Only the substring up to the first @ in the password is masked. Remaining password characters appear in the output after the mask marker, as if they were a hostname component.

Suggested Fix

Use a greedy last-@ match to correctly identify the host separator:

# Use a non-greedy approach that matches up to the LAST @ before the host
masked = re.sub(
    r"(://[^:]+:)(.+)(@[^@]*/)",  # match up to last @ followed by host/
    r"\1***\3",
    url,
)

Or more robustly, use urllib.parse.urlparse to extract and mask the password component:

from urllib.parse import urlparse, urlunparse
parsed = urlparse(url)
if parsed.password:
    # Reconstruct netloc with masked password
    netloc = parsed.netloc.replace(f":{parsed.password}@", ":***@", 1)
    masked = urlunparse(parsed._replace(netloc=netloc))
else:
    masked = url

Category

security

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD.


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

## Bug Report: Security — `mask_database_url` Partial Mask for Passwords Containing `@` ### Severity Assessment - **Impact**: Database credentials with `@` in the password are only partially masked. The portion of the password after the `@` is exposed in the logged/displayed URL, leaking sensitive credential data. The output URL is also structurally malformed. - **Likelihood**: Medium — passwords with `@` are valid and common (e.g., generated passwords, URL-encoded chars decoded too early). - **Priority**: High ### Location - **File**: `src/cleveragents/shared/redaction.py` - **Function/Class**: `mask_database_url` - **Lines**: 210–214 ### Description The masking regex uses `[^@]+` to match the password field, which stops at the **first** `@` character. If a database password legitimately contains an `@` (which is valid per RFC 3986 when percent-encoded, or via direct inclusion in some drivers), only the portion before the first `@` is masked. The rest of the password appears verbatim after the substitution. ### Evidence ```python # redaction.py lines 210–214 masked = re.sub( r"(://[^:]+:)([^@]+)(@)", # [^@]+ stops at FIRST @ in password! r"\1***\3", url, ) ``` Demonstration: ```python url = "postgresql://user:pass@word@localhost/db" result = mask_database_url(url) # result == "postgresql://user:***@word@localhost/db" # BUG: "word" portion of password "pass@word" is exposed! ``` Expected output: the full password should be replaced, yielding `postgresql://user:***@localhost/db`. ### Expected Behavior The entire password portion (everything between `:` after the username and the `@` before the hostname) is replaced with `***`, regardless of whether the password contains `@` characters. ### Actual Behavior Only the substring up to the first `@` in the password is masked. Remaining password characters appear in the output after the mask marker, as if they were a hostname component. ### Suggested Fix Use a greedy last-`@` match to correctly identify the host separator: ```python # Use a non-greedy approach that matches up to the LAST @ before the host masked = re.sub( r"(://[^:]+:)(.+)(@[^@]*/)", # match up to last @ followed by host/ r"\1***\3", url, ) ``` Or more robustly, use `urllib.parse.urlparse` to extract and mask the password component: ```python from urllib.parse import urlparse, urlunparse parsed = urlparse(url) if parsed.password: # Reconstruct netloc with masked password netloc = parsed.netloc.replace(f":{parsed.password}@", ":***@", 1) masked = urlunparse(parsed._replace(netloc=netloc)) else: masked = url ``` ### Category security ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-12 03:44:46 +00:00
Author
Owner

Verified — Security bug: mask_database_url fails when password contains '@'. MoSCoW: Must-have. Priority: High — security vulnerability.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Security bug: mask_database_url fails when password contains '@'. MoSCoW: Must-have. Priority: High — security vulnerability. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Security bug: mask_database_url fails when password contains '@'. MoSCoW: Must-have. Priority: High — security vulnerability.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Security bug: mask_database_url fails when password contains '@'. MoSCoW: Must-have. Priority: High — security vulnerability. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Security bug: mask_database_url fails when password contains '@'. MoSCoW: Must-have. Priority: High — security vulnerability.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Security bug: mask_database_url fails when password contains '@'. MoSCoW: Must-have. Priority: High — security vulnerability. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
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#7761
No description provided.