Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@
**Vulnerability:** Server-Side Request Forgery (SSRF) / Local File Inclusion
**Learning:** External URL fetching with `urllib.request.urlopen` (like API endpoints passed via environment variables) can accept schemes like `file://` implicitly, which could allow arbitrary file reading or internal network scanning if the environment is misconfigured or manipulated.
**Prevention:** Always validate that URLs explicitly start with `http://` or `https://` before using them in standard library requests. Append to suppress linter warnings only after verifying the input is validated.
## 2025-02-24 - [Information Leakage on Timeout]
**Vulnerability:** Information Leakage on Subprocess Timeout in sandboxed_verify.py
**Learning:** When running subprocesses with a timeout in Python using `subprocess.run`, the resulting `subprocess.TimeoutExpired` exception object contains unmasked `stdout` and `stderr` data. The `sandboxed_verify.py` script directly printed this data on timeout, potentially exposing sensitive environment variables or secrets injected during the execution.
**Prevention:** Ensure all subprocess output, including output retrieved from exception objects (like `TimeoutExpired`), is explicitly sanitized using a token scrubbing function before logging or printing to `stdout/stderr`.
27 changes: 23 additions & 4 deletions scripts/ci/sandboxed_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,25 @@
)
RESULT_MARKER = "SANDBOXED_VERIFY_RESULT"
ENV_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
SENSITIVE_DATA_SCRUB_PATTERNS = (
(re.compile(r'(?i)(bearer\s+)[^\s"\'\\]+'), r'\1***'),
(re.compile(r'(?i)(token\s+)[^\s"\'\\]+'), r'\1***'),
(re.compile(r'(?i)\b(?:github_pat_[A-Za-z0-9_]+|gh[pousr]_[A-Za-z0-9_]+)\b'), '***'),
(re.compile(r'\b(sk-[A-Za-z0-9_-]+)'), '***'),
(re.compile(r'\b(xox[baprs]-[A-Za-z0-9-]+)'), '***'),
(re.compile(r'\b(AKIA[0-9A-Z]{16})'), '***'),
(re.compile(r'(?i)((?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|password|passwd|secret)\s*[:=]\s*)["\']?[^"\'\s]+["\']?'), r'\1***'),
(re.compile(r'(?i)((?:authorization|proxy-authorization)\s*:\s*(?:bearer|basic)\s+)[A-Za-z0-9._~+\/=-]+'), r'\1***'),
)


def scrub_sensitive_data(text: str | None) -> str | None:
"""Mask sensitive tokens in text to prevent secret leakage."""
if not text:
return text
for pattern, repl in SENSITIVE_DATA_SCRUB_PATTERNS:
text = pattern.sub(repl, text)
return text


def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
Expand Down Expand Up @@ -219,13 +238,13 @@ def main(argv: Sequence[str] | None = None) -> int:
try:
completed = run_command(args.command, copied_repo, env, args.timeout)
if completed.stdout:
print(completed.stdout, end="")
print(scrub_sensitive_data(completed.stdout), end="")
if completed.stderr:
print(completed.stderr, end="", file=sys.stderr)
print(scrub_sensitive_data(completed.stderr), end="", file=sys.stderr)
exit_code = completed.returncode
except subprocess.TimeoutExpired as exc:
stdout = timeout_output_text(exc.stdout)
stderr = timeout_output_text(exc.stderr)
stdout = scrub_sensitive_data(timeout_output_text(exc.stdout))
stderr = scrub_sensitive_data(timeout_output_text(exc.stderr))
if stdout:
print(stdout, end="" if stdout.endswith("\n") else "\n")
if stderr:
Expand Down
8 changes: 8 additions & 0 deletions tests/test_sandboxed_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ def test_timeout_output_text_normalizes_subprocess_payloads():
assert sandboxed_verify.timeout_output_text("text-output") == "text-output"


def test_scrub_sensitive_data():
"""Ensure sensitive data is masked."""
assert sandboxed_verify.scrub_sensitive_data(None) is None
assert sandboxed_verify.scrub_sensitive_data("bearer token123") == "bearer ***"
assert sandboxed_verify.scrub_sensitive_data("GH_TOKEN=ghp_abc123") == "GH_TOKEN=***"
assert sandboxed_verify.scrub_sensitive_data("normal text") == "normal text"


def test_main_runs_command_in_copy_without_mutating_source(tmp_path, capsys):
"""The wrapper runs commands in the copied workspace, not the source tree."""
repo = tmp_path / "repo"
Expand Down