Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -891,7 +891,7 @@ jobs:

# Recognized signals that the LLM backend was unavailable / starved.
backend_unavailable_signal='RateLimitError|Too many requests\. For more on scraping GitHub|exceeded your current quota|insufficient_quota|billing details|"status"[[:space:]]*:[[:space:]]*"RESOURCE_EXHAUSTED"|tokens_limit_reached|Request body too large|Max size:[[:space:]]*[0-9]+[[:space:]]+tokens|Error code:[[:space:]]*413|LLM CONNECTION FAILED|Could not establish connection to the language model|LLM warm-up failed|Configured model and fallback models were unavailable|Configured Vertex model and fallback models were unavailable|emitted provider infrastructure or failure-signal output|before provider infrastructure failure|litellm(\.exceptions)?\.NotFoundError[^[:cntrl:]]*Nvidia_nimException[^[:cntrl:]]*Error code:[[:space:]]*404|Error during penetration test: loginAsGuest failed after [0-9]+ attempts: curl exit 7: curl: \(7\) Failed to connect to 127\.0\.0\.1 port 48080'
model_behavior_error_signal='(^|[^A-Za-z0-9_])(agents|pydantic_ai|strix)(\.[A-Za-z_][A-Za-z0-9_]*)*\.ModelBehaviorError([^A-Za-z0-9_]|$)'
model_behavior_error_signal='(^|[^A-Za-z0-9_])(agents|pydantic_ai|strix)(\.[A-Za-z_][A-Za-z0-9_]*)*\.ModelBehaviorError([^A-Za-z0-9_]|$)|ended without calling finish_scan\. The agent emitted a text-only turn instead of a lifecycle tool call'
# Any evidence that a vulnerability was actually reported. Its presence
# forces a hard failure so real findings are NEVER downgraded. Keep the
# severity branch anchored away from identifiers so environment lines
Expand Down
12 changes: 12 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,15 @@
**Vulnerability:** Command Injection
**Learning:** Fixing a `shell=True` vulnerability by replacing it with `shell=False` and wrapping the command string in `["/bin/bash", "-lc", command]` is incomplete and still leaves the code vulnerable to shell injection. It acts as security theater, as it misleads linters while executing untrusted input via the bash wrapper. The vulnerability was still present in `sandboxed_web_e2e.py`.
**Prevention:** Remove `/bin/bash` wrapper from `subprocess` calls in CI scripts. Always use `shlex.split(command)` to safely parse strings into a list of arguments and pass the list directly to `subprocess.Popen` or `subprocess.run`.
## 2026-08-24 - SSRF Vulnerability in sandboxed_web_e2e.py
**Vulnerability:** The `wait_for_url` function in `scripts/ci/sandboxed_web_e2e.py` did not validate the URL hostname before making requests, creating a Server-Side Request Forgery (SSRF) risk.
**Learning:** Arbitrary URLs passed to internal utilities must be rigorously validated, especially in CI environments, to ensure they do not access unintended network locations or internal services.
**Prevention:** Always use `urllib.parse.urlparse` to validate that the parsed URL hostname is restricted to safe loopback addresses (e.g., `localhost` or `127.0.0.1`) before opening the URL, particularly for sandbox or internal healthcheck endpoints.
## 2026-08-24 - Classify Strix Text-Only Turn as ModelBehaviorError
**Vulnerability:** Unreliable LLM Fallback Mechanism
**Learning:** If the agent fails to call the `finish_scan` tool and instead emits a plain-text turn, the Strix workflow fails. If this failure isn't mapped to `ModelBehaviorError`, the CI system treats it as a hard CI failure rather than falling back to alternative models.
**Prevention:** Include `ended without calling finish_scan` in the regex for `ModelBehaviorError` in `.github/workflows/strix.yml` and `scripts/ci/strix_quick_gate.sh` so that the pipeline can gracefully fall back to a stronger model.
## 2026-08-24 - Explicit shell=False in Subprocess Calls
**Vulnerability:** Subprocess Command Injection
**Learning:** Even when using `shlex.split()` to separate command arguments, if `shell=False` is not explicitly defined in `subprocess.Popen` and `subprocess.run` calls, security scanners like Bandit and Strix may flag the code as vulnerable to command injection.
**Prevention:** Always explicitly define `shell=False` when invoking `subprocess.Popen` or `subprocess.run` with untrusted commands, even when pre-processing with `shlex.split()`, to ensure robust safety and satisfy strict security linting.
6 changes: 6 additions & 0 deletions scripts/ci/sandboxed_web_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
from collections.abc import Sequence
from dataclasses import dataclass
Expand Down Expand Up @@ -110,6 +111,7 @@ def start_service(label: str, command: str, cwd: Path, env: dict[str, str], logs
stdout=log_file,
stderr=subprocess.STDOUT,
start_new_session=True,
shell=False,
)
log_file.close()
return Service(label=label, command=command, process=process, log_path=log_path)
Expand All @@ -121,6 +123,9 @@ def wait_for_url(url: str, timeout: int, service: Service) -> bool:
return True
if not (url.startswith("http://") or url.startswith("https://")):
raise ValueError(f"URL must start with http:// or https://, got: {url}")
parsed = urllib.parse.urlparse(url)
if parsed.hostname not in {"localhost", "127.0.0.1"}:
raise ValueError(f"URL hostname must be restricted to safe loopback addresses, got: {parsed.hostname}") # pragma: no cover
deadline = time.monotonic() + timeout
opener = urllib.request.build_opener(NoRedirectHandler())
while time.monotonic() < deadline:
Expand All @@ -146,6 +151,7 @@ def run_shell(command: str, cwd: Path, env: dict[str, str], timeout: int) -> sub
stderr=subprocess.PIPE,
timeout=timeout,
check=False,
shell=False,
)


Expand Down
2 changes: 1 addition & 1 deletion scripts/ci/strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2872,7 +2872,7 @@ is_model_behavior_error() {
# Classify only a module-qualified Strix/Agents SDK protocol exception.
# A bare source-file mention of ModelBehaviorError is not retryable.
# Cross-model fallback may continue; same-model retry does not.
if grep -Eq '(^|[^A-Za-z0-9_])(agents|pydantic_ai|strix)(\.[A-Za-z_][A-Za-z0-9_]*)*\.ModelBehaviorError([^A-Za-z0-9_]|$)' "$STRIX_LOG"; then
if grep -Eq '(^|[^A-Za-z0-9_])(agents|pydantic_ai|strix)(\.[A-Za-z_][A-Za-z0-9_]*)*\.ModelBehaviorError([^A-Za-z0-9_]|$)' "$STRIX_LOG" || grep -Fq "ended without calling finish_scan. The agent emitted a text-only turn instead of a lifecycle tool call" "$STRIX_LOG"; then
Comment thread
seonghobae marked this conversation as resolved.
return 0
fi

Expand Down
12 changes: 9 additions & 3 deletions tests/test_sandboxed_web_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,13 @@ def fake_run(*args, **kwargs):
assert service.command == "npm run dev"
assert service.log_path == tmp_path / "backend.log"
assert popen_calls[0][0] == (["npm", "run", "dev"],)
assert "shell" not in popen_calls[0][1]
assert popen_calls[0][1].get("shell") is False
assert "executable" not in popen_calls[0][1]
assert popen_calls[0][1]["start_new_session"] is True
assert completed.returncode == 7
assert run_calls[0][0] == (["npm", "test"],)
assert run_calls[0][1]["timeout"] == 5
assert "shell" not in run_calls[0][1]

assert "executable" not in run_calls[0][1]


Expand Down Expand Up @@ -232,7 +232,7 @@ def test_no_redirect_handler_raises_httperror_without_following():
"""Readiness checks must raise HTTPError on redirects to prevent attacker-controlled internal URLs."""
import urllib.error

request = sandboxed_web_e2e.urllib.request.Request("https://example.test/ready")
request = sandboxed_web_e2e.urllib.request.Request("http://127.0.0.1/ready")

with pytest.raises(urllib.error.HTTPError) as exc_info:
sandboxed_web_e2e.NoRedirectHandler().redirect_request(request, None, 302, "Found", {}, "http://127.0.0.1")
Expand Down Expand Up @@ -598,3 +598,9 @@ def test_module_import_and_main_entrypoint(monkeypatch, tmp_path):
if module is not None:
sys.modules["scripts.ci.sandboxed_web_e2e"] = module
assert exc_info.value.code == 0

def test_wait_for_url_rejects_non_loopback_hostnames():
from scripts.ci import sandboxed_web_e2e
import pytest
with pytest.raises(ValueError, match="URL hostname must be restricted to safe loopback addresses"):
sandboxed_web_e2e.wait_for_url("http://example.com/ready", 1, None)
9 changes: 9 additions & 0 deletions tests/test_strix_model_behavior_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,5 +222,14 @@ def test_quality_trigger_includes_model_behavior_contracts(self) -> None:
self.assertIn(' - "tests/test_strix_model_behavior_error.py"', workflow)



def test_finish_scan_text_turn_is_retryable(self) -> None:
"""Recognize the explicit text-turn failure as retryable."""
log = (
"ended without calling finish_scan. The agent emitted a text-only turn instead of a lifecycle tool call\n"
"Vulnerabilities 0\n"
)
self.assertTrue(_classifies_as_model_behavior_error(log))

if __name__ == "__main__":
unittest.main()
Loading