fix(#829): guard prefixed runtime variable launches
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
ms-lead-reviewer
2026-07-18 00:39:13 -05:00
parent 91a4a98370
commit 04cc065c2b
5 changed files with 108 additions and 23 deletions

View File

@@ -79,7 +79,7 @@ DIRECT_PATTERNS: Final = (
re.compile(r"\beval\s+[\"'](?:claude|pi)(?:\s|[\"'])"),
)
RUNTIME_ASSIGNMENT: Final = re.compile(
r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*[\"']?(?:claude|pi)(?:\s|[\"']|$)"
r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*[\"']?(?:claude|pi)(?:\s|[\"']|[;&|]|$)"
)
TYPESCRIPT_WRAPPER_INVOCATION: Final = re.compile(
r"(?m)^\s*execRuntime\s*\(\s*[\"']python3[\"']\s*,\s*"
@@ -226,44 +226,71 @@ def is_gated_line(path: Path, line: str) -> bool:
)
def is_shell_direct_invocation(path: Path, line: str) -> bool:
def shell_command_tokens(path: Path, line: str) -> list[str]:
if path.suffix.lower() not in SHELL_SUFFIXES:
return False
return []
assignment = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
case_arm = re.match(r"^\s*[^;&()]+\)\s*", line)
command_source = line[case_arm.end() :] if case_arm is not None else line
resolved: list[str] = []
for command in shell_commands(command_source):
index = 0
while index < len(command) and assignment.match(command[index]):
index += 1
while index < len(command) and command[index] in {"command", "exec", "nohup"}:
index += 1
if index < len(command) and command[index] == "env":
index += 1
while index < len(command) and (
command[index].startswith("-") or assignment.match(command[index])
):
# Resolve command position once for every literal and variable caller.
# Prefixes may be nested in either order (for example `exec env A=1`).
while index < len(command):
if command[index] in {"command", "exec", "nohup"}:
index += 1
while index < len(command) and command[index] in {"command", "exec", "nohup"}:
continue
if command[index] == "env":
index += 1
if index < len(command) and Path(command[index]).name in {"claude", "pi"}:
return True
return False
while index < len(command) and (
command[index].startswith("-") or assignment.match(command[index])
):
index += 1
continue
break
if index < len(command):
resolved.append(command[index])
return resolved
def is_direct_line(path: Path, line: str) -> bool:
return is_shell_direct_invocation(path, line) or any(
def runtime_variable_reference(token: str, runtime_variables: set[str]) -> bool:
match = re.fullmatch(r"\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})", token)
if match is None:
return False
return (match.group(1) or match.group(2)) in runtime_variables
def is_shell_direct_invocation(
path: Path, line: str, runtime_variables: set[str]
) -> bool:
return any(
Path(token).name in {"claude", "pi"}
or runtime_variable_reference(token, runtime_variables)
for token in shell_command_tokens(path, line)
)
def is_direct_line(path: Path, line: str, runtime_variables: set[str]) -> bool:
return is_shell_direct_invocation(path, line, runtime_variables) or any(
pattern.search(line) for pattern in DIRECT_PATTERNS
)
def executes_runtime_variable(line: str, runtime_variables: set[str]) -> bool:
def executes_runtime_variable(
path: Path, line: str, runtime_variables: set[str]
) -> bool:
if any(
runtime_variable_reference(token, runtime_variables)
for token in shell_command_tokens(path, line)
):
return True
for variable in runtime_variables:
reference = rf"\$(?:{re.escape(variable)}|\{{{re.escape(variable)}\}})"
if re.search(rf"\beval\s+[\"']?{reference}", line):
return True
if re.match(rf"^\s*[\"']?{reference}[\"']?(?:\s|$)", line):
return True
return False
@@ -293,8 +320,8 @@ def classify_text(path: Path, source: str) -> list[LaunchSite]:
if assignment is not None:
runtime_variables.add(assignment.group(1))
primitive_violation = DANGEROUS_PRIMITIVE in line and not is_choke_point(path)
line_is_direct = is_direct_line(path, line) or executes_runtime_variable(
line, runtime_variables
line_is_direct = is_direct_line(path, line, runtime_variables) or executes_runtime_variable(
path, line, runtime_variables
)
line_is_gated = line_number in validated_typescript_wrappers or is_gated_line(path, line)