feat(safety): collect target-list files passed via -w/-l flags

Recon tools route their target list through a flag — `ffuf -w wordlist.txt`,
`httpx -l hosts.txt`, `nuclei --list targets.txt` — not the `<` redirect the
input-file collector already handled, so the reviewer kept blocking "probes
every host listed in hosts.txt" because the list was never in the packet. Parse
the value of the common list-file flags and collect it the same bounded way,
alongside redirect inputs. The value is only read when it resolves to a
workspace file, so a boolean `-l` (grep, wc) whose next token is not a file
collects nothing and never makes the packet incomplete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
oyasumi
2026-08-08 17:57:08 +00:00
co-authored by Claude Opus 5
parent dad35b9e5f
commit 3c4be45d34
3 changed files with 91 additions and 6 deletions
+7 -6
View File
@@ -82,12 +82,13 @@ A command that runs code Strix cannot resolve to an inspectable script — an
unrecognized interpreter, or an interpreter given no script — is blocked rather
than reviewed against an empty evidence packet.
When a command reads a workspace data file through input redirection (for
example a host list consumed by `while read … done < hosts.txt`), that file's
contents are attached to the packet so the reviewer can check the entries —
queried hosts, fuzz inputs against scope instead of blocking because it can't
see them. Only workspace-resident files are read; an oversize file is attached
truncated. An authorized domain covers its subdomains.
When a command reads a workspace data file through input redirection
(`while read … done < hosts.txt`) or a target-list flag (`ffuf -w words.txt`,
`httpx -l hosts.txt`) — that file's contents are attached to the packet so the
reviewer can check the entries, queried hosts or fuzz inputs, against scope
instead of blocking because it can't see them. Only workspace-resident files are
read; an oversize file is attached truncated. An authorized domain covers its
subdomains.
Browser automation inside scripts is blocked in safety modes. Issue browser
operations as individual raw `agent-browser` commands so each action can be
+41
View File
@@ -33,6 +33,24 @@ _SHELL_SEPARATOR_CHARS = frozenset(";&|\n\r")
_REDIRECT_INPUT_RE = re.compile(
r"""(?<![<])\d?<(?![<(])\s*(?P<file>"[^"]+"|'[^']+'|[^\s;|&<>()]+)""",
)
# Flags whose value is a file of targets a command reads — wordlists, host lists.
# Recon tools (ffuf, httpx, nuclei, subfinder, dnsx, gobuster) route their input this
# way rather than through a `<` redirect, so the same evidence must be collected. The
# value is only read when it resolves to a workspace file, so a boolean `-l` (wc, grep)
# whose next token is not a file collects nothing.
_LIST_FILE_FLAGS = frozenset(
{
"-w",
"-l",
"-iL",
"-list",
"-wordlist",
"--list",
"--wordlist",
"--input-file",
"--input",
}
)
_SCRIPT_SUFFIXES = (".py", ".sh", ".bash", ".js", ".mjs", ".rb", ".pl")
# `awk` is intentionally absent: its program is an inline positional argument, not a
# `-c`/script-file the entrypoint reader can resolve, so it belongs with the data tools.
@@ -810,6 +828,9 @@ def parse_command(command: str) -> CommandPlan:
executable = PurePosixPath(tokens[index]).name
args = tokens[index + 1 :]
plan.executable = executable
for candidate in _list_flag_files(args):
if candidate not in plan.input_files:
plan.input_files.append(candidate)
if executable in _OPAQUE_WRAPPERS:
plan.parse_error = (
@@ -980,6 +1001,26 @@ def _redirect_input_files(command: str) -> list[str]:
return files
def _list_flag_files(args: list[str]) -> list[str]:
"""Files named as the value of a target-list flag (`-w wordlist`, `-l hosts`)."""
files: list[str] = []
index = 0
while index < len(args):
name, separator, inline = args[index].partition("=")
if name in _LIST_FILE_FLAGS:
if separator:
value = inline
elif index + 1 < len(args):
value = args[index + 1]
index += 1
else:
value = ""
if value and not value.startswith("-") and value not in files:
files.append(value)
index += 1
return files
async def _read_sandbox_file(session: Any, path: PurePosixPath, limit: int) -> bytes:
stream = await session.read(Path(path.as_posix()))
try:
+43
View File
@@ -916,3 +916,46 @@ def test_data_tools_reading_script_named_files_are_not_execution(command: str) -
"""A read/transfer/text tool takes a script-named file as data, not as a program to
run, so it must not trip the unresolved-execution guard."""
assert parse_command(command).parse_error is None
@pytest.mark.parametrize(
("command", "expected"),
[
("ffuf -w /workspace/words.txt -u https://x/FUZZ", ["/workspace/words.txt"]),
("httpx -l hosts.txt -sc -title", ["hosts.txt"]),
("nuclei --list targets.txt -severity high", ["targets.txt"]),
("ffuf -w=words.txt -u https://x/FUZZ", ["words.txt"]),
("subfinder -d x -o out.txt", []), # -o is output, not a list input
],
)
def test_list_flag_files_are_parsed(command: str, expected: list[str]) -> None:
assert parse_command(command).input_files == expected
@pytest.mark.asyncio
async def test_wordlist_flag_file_is_attached_for_scope_review() -> None:
"""Recon tools route their target list through `-w`/`-l`, not a `<` redirect, so the
same evidence must be collected for the reviewer to check it against scope."""
bundle = await _compile(
"ffuf -w /workspace/paths.txt -u https://api.fiuu.com/FUZZ -mc all",
{"/workspace/paths.txt": "admin\napi\nlogin\n"},
workdir="/workspace",
)
try:
inputs = [a for a in bundle.packet["artifacts"] if a.get("role") == "input"]
assert [a["path"] for a in inputs] == ["/workspace/paths.txt"]
assert "admin" in inputs[0]["source"]
finally:
bundle.cleanup()
@pytest.mark.asyncio
async def test_list_flag_value_that_is_not_a_workspace_file_collects_nothing() -> None:
"""A boolean `-l` (grep, wc) whose next token is not a workspace file must not make
the packet incomplete or attach anything."""
bundle = await _compile("grep -l pattern /workspace/app.py", workdir="/workspace")
try:
assert [a for a in bundle.packet["artifacts"] if a.get("role") == "input"] == []
assert bundle.deterministic_block is None
finally:
bundle.cleanup()