Skip to content
Draft
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
47bab55
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 18, 2026
7ab5ba1
ci: repair command-injection detector review findings
seonghobae Aug 18, 2026
fe3bf9e
fix(sast): distinguish Python shell execution APIs
github-actions[bot] Aug 18, 2026
b97ab9f
ci: remove completed command-injection repair authority
seonghobae Aug 18, 2026
12350c8
ci: verify nested subprocess command-injection detection
seonghobae Aug 18, 2026
c0ed77c
ci: replace nested subprocess regex with AST validation
seonghobae Aug 18, 2026
0914323
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 18, 2026
4b5d402
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 18, 2026
aee3876
ci: verify final command-injection detector repair
seonghobae Aug 19, 2026
b0dfcb6
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 19, 2026
ac59920
ci: verify AST-backed Python shell detection
seonghobae Aug 20, 2026
dd681ab
ci: checkout the repair branch before verification
seonghobae Aug 20, 2026
588469f
chore(ci): remove completed PR 983 repair authority
seonghobae Aug 20, 2026
9512361
ci: execute bounded AST command-injection repair
seonghobae Aug 20, 2026
abb1b52
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 20, 2026
5272458
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 20, 2026
952fa20
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 20, 2026
40c4d24
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 20, 2026
9a3b17e
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 20, 2026
56e15f1
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 20, 2026
3f9db72
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 20, 2026
c8cf4d2
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 20, 2026
d484a85
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 21, 2026
59dcc82
πŸ›‘οΈ Sentinel: detect deeply nested Python shell calls
seonghobae Aug 21, 2026
bdc529e
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 21, 2026
46cbbd8
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Aug 26, 2026
ae7899f
Merge branch 'develop' into sentinel-fix-python-command-injection-reg…
opencode-agent[bot] Aug 26, 2026
982abcb
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Sep 26, 2026
45d4eab
merge: preserve protected threat-model and scanner fix
seonghobae Sep 26, 2026
f29064a
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Sep 26, 2026
50afdd6
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Sep 28, 2026
ec65860
πŸ›‘οΈ Sentinel: [CRITICAL] Fix python command injection rule to detect o…
seonghobae Sep 28, 2026
935ec31
πŸ›‘οΈ Sentinel: detect deeply nested Python shell calls
seonghobae Sep 29, 2026
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 @@ -127,3 +127,7 @@
**Vulnerability:** The `/api/v1/webhook` POST endpoint in `appguardrail_core/controlplane.py` failed to validate the `url` property when accepting it into the database, leading to Stored SSRF risks. In addition, the core SSRF validation logic (`_is_safe_url`) in both the CLI and control-plane did not verify the input type (e.g. `isinstance(url, str)`). Passing non-string types (like integers) resulted in unhandled `AttributeError` exceptions inside `urllib.parse.urlparse`, which led to API 500 crashes on malicious JSON payloads.
**Learning:** Network endpoints must explicitly validate the data type of user-provided configurations prior to execution or storage. Furthermore, webhooks configured by users should always be checked for SSRF when saved, as trusting them later assumes input has already been safely validated, bypassing downstream network guardrails.
**Prevention:** Apply `_is_safe_url` checks directly upon ingestion (e.g., in `/api/v1/webhook`) and enforce type checks `if not isinstance(url, str): return False` prior to using library parsing functions like `urlparse`. Always return gracefully failing responses (like `400 Bad Request`) for unsafe URLs instead of allowing unhandled 500 server errors.
## 2026-08-05 - Missing os.system command injection detection
**Vulnerability:** The static analysis scanner's `python-command-injection` rule failed to detect command injections via `os.system` and `os.popen`.
**Learning:** The previous regular expression assumed `os.system` and `os.popen` accepted a `shell=True` argument similar to `subprocess.run`, leading to false negatives as they execute via the shell implicitly and don't accept `shell=True`.
**Prevention:** Ensure static analysis regex rules handling different APIs group them accurately according to their respective signatures rather than merging them under a single mandatory condition (like `shell=True`). Added tests to prevent regression.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- λŒ€μ‹œλ³΄λ“œ 검색창 μ»€μ„œ μœ μ§€ β€” 검색어 μ€‘κ°„μ—μ„œ ν…μŠ€νŠΈλ₯Ό μˆ˜μ •ν•  λ•Œλ§ˆλ‹€ μ»€μ„œκ°€ κ²€μƒ‰μ–΄μ˜ 맨 끝으둜 μ ν”„ν•˜λŠ” λΆˆνŽΈν•¨μ„ μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€. 이제 μž…λ ₯창의 μ»€μ„œ μœ„μΉ˜(`selectionStart`/`selectionEnd`)κ°€ 동적 λ Œλ”λ§ 이후에도 μ›λž˜ μœ„μΉ˜μ— μ •ν™•νžˆ μœ μ§€λ˜μ–΄ μžμ—°μŠ€λŸ¬μš΄ 타이핑 κ²½ν—˜μ„ μ œκ³΅ν•©λ‹ˆλ‹€.

### λ³΄μ•ˆ
- Python λͺ…λ Ή μ£Όμž… 탐지가 `os.system`Β·`os.popen`의 암묡적 μ…Έ μ‹€ν–‰κ³Ό `subprocess(..., shell=True)`λ₯Ό μ •ν™•νžˆ ꡬ뢄해 μ„€λͺ…ν•˜κ³ , μ€‘μ²©λœ λͺ…λ Ή 생성 호좜 λ’€μ˜ `shell=True`κΉŒμ§€ νƒμ§€ν•˜λ„λ‘ λ³΄κ°•ν–ˆμŠ΅λ‹ˆλ‹€. 각 API와 λΉ„μ…Έ `subprocess` μŒμ„± 사둀λ₯Ό 독립 νšŒκ·€λ‘œ κ²€μ¦ν•©λ‹ˆλ‹€.
- 리포트 좜λ ₯ ν•˜λ“œλ‹ β€” μƒμ„±λœ markdown λ¦¬ν¬νŠΈκ°€ HTML둜 λ Œλ”λ  λ•Œ μ•…μ„± finding λ‚΄μš©(예: μ™ΈλΆ€ 엔진이 μŠ€μΊ”ν•œ μ½”λ“œμ˜ `<script>`)이 μ£Όμž…λ˜μ§€ μ•Šλ„λ‘, ν”„λ‘œμ¦ˆ ν•„λ“œ(message/remediation/verification)λ₯Ό HTML μ΄μŠ€μΌ€μ΄ν”„ν•˜κ³  snippet의 code-fence νƒˆμΆœμ„ 무λ ₯ν™”ν•©λ‹ˆλ‹€(λͺ¨λ“  리포트 νƒ€μž…). rule_id/category/context λ“± μ œμ•½λœ μ‹λ³„μžλŠ” κ·ΈλŒ€λ‘œ λ‘‘λ‹ˆλ‹€.
- control plane API ν•˜λ“œλ‹: (1) μš”μ²­ 본문을 10MiB둜 μΊ‘ν•˜κ³  음수 Content-Lengthλ₯Ό κ±°λΆ€ν•©λ‹ˆλ‹€(유효 ν‚€ μ†Œμ§€μžμ˜ OOM/EOF-hang λ°©μ§€). (2) `limit`/`offset` 쿼리 νŒŒλΌλ―Έν„°λ₯Ό ν΄λž¨ν”„ν•©λ‹ˆλ‹€ β€” sqliteμ—μ„œ `LIMIT -1`은 λ¬΄μ œν•œμ΄λ―€λ‘œ 음수λ₯Ό κ·ΈλŒ€λ‘œ μ „λ‹¬ν•˜λ©΄ νŽ˜μ΄μ§€λ„€μ΄μ…˜ 캑이 μš°νšŒλμŠ΅λ‹ˆλ‹€(list 1..1000, trend 1..365, offset β‰₯0).

Expand Down
4 changes: 2 additions & 2 deletions scanner/cli/appguardrail.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,10 +454,10 @@ def _console_print(*values, **kwargs) -> None:
{
"id": "python-command-injection",
"pattern": re.compile(
r"(?i)(?:os\.system|subprocess\.(?:Popen|run|call|check_call|check_output))\s*\([^)]*shell\s*=\s*True"
r"(?is)(?:\bos\.(?:system|popen)\s*\(|\bsubprocess\.(?:Popen|run|call|check_call|check_output)\s*\((?:[^()]|\([^()]*\)){0,1200}\bshell\s*=\s*True)"
),
"severity": "CRITICAL",
"message": "Potential Command Injection detected: shell=True used in Python subprocess/os command. [OWASP A03:2021 - Injection]",
"message": "Potential command injection sink detected: os.system/os.popen execute through a shell, and subprocess shell=True invokes a shell. Avoid shell execution, validate untrusted input, and use argument arrays where possible. [OWASP A03:2021 - Injection]",
"extensions": [".py"],
},
{
Expand Down
81 changes: 81 additions & 0 deletions tests/test_appguardrail.py
Original file line number Diff line number Diff line change
Expand Up @@ -1828,3 +1828,84 @@ def test_cli_routes_console_calls_through_accessibility_wrapper():
]

assert direct_print_lines == []


@pytest.mark.parametrize(
("filename", "source"),
[
pytest.param(
"os_system.py",
"os.system(user_input)\n",
id="os-system",
),
pytest.param(
"os_popen.py",
"os.popen(command)\n",
id="os-popen",
),
pytest.param(
"subprocess_shell.py",
"subprocess.call(command, shell=True)\n",
id="subprocess-shell",
),
pytest.param(
"subprocess_nested_shell.py",
"subprocess.call(build_command(user_input), shell=True)\n",
id="nested-subprocess-shell",
),
],
)
def test_python_shell_spawning_apis_are_detected_independently(
tmp_path: Path,
filename: str,
source: str,
) -> None:
"""Detect each Python shell-spawning API without shared-fixture masking."""
target = tmp_path / filename
target.write_text(source, encoding="utf-8")

findings = _scan_file(target, tmp_path)

assert [item["rule_id"] for item in findings].count(
"python-command-injection"
) == 1


def test_python_shell_spawning_rule_describes_both_shell_mechanisms(
tmp_path: Path,
) -> None:
"""Explain implicit os shells separately from subprocess shell=True."""
target = tmp_path / "os_system.py"
target.write_text("os.system(user_input)\n", encoding="utf-8")

findings = [
item
for item in _scan_file(target, tmp_path)
if item["rule_id"] == "python-command-injection"
]

assert len(findings) == 1
assert "os.system/os.popen execute through a shell" in findings[0]["message"]
assert "subprocess shell=True" in findings[0]["message"]


@pytest.mark.parametrize(
"source",
[
"subprocess.call(command)\n",
"subprocess.call(command, shell=False)\n",
],
)
def test_python_shell_spawning_rule_ignores_non_shell_subprocess_calls(
tmp_path: Path,
source: str,
) -> None:
"""Do not classify ordinary subprocess calls as shell execution."""
target = tmp_path / "subprocess_safe.py"
target.write_text(source, encoding="utf-8")

findings = _scan_file(target, tmp_path)

assert "python-command-injection" not in {
item["rule_id"] for item in findings
}
Loading