Block dispatch payload shell injection

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-01 14:30:31 +01:00
parent 4d2b312457
commit 97a39e8819
2 changed files with 42 additions and 6 deletions
+15 -6
View File
@@ -24,14 +24,22 @@ USES_RE = re.compile(
)
RUN_RE = re.compile(rf"^(\s*)(?:-\s*)?{_yaml_key('run')}\s*:\s*(.*)$")
EXPRESSION_RE = re.compile(r"\$\{\{(.*?)\}\}")
# Workflow-call and dispatch inputs are data, not shell source. Step and job
# outputs are data too: they can carry event or input values across an
# otherwise-safe intermediate step. Secrets include github.token because
# Workflow-call and dispatch inputs are data, not shell source. The legacy
# github.event.inputs alias is identical data, and repository_dispatch callers
# fully control client_payload. Step and job outputs are data too: they can
# carry event or input values across an otherwise-safe intermediate step.
# Whole github contexts are unsafe because they include event data (and the
# github context includes github.token). Secrets include github.token because
# Actions makes that credential available independently of an explicit
# secrets.GITHUB_TOKEN reference.
SHELL_DATA_CONTEXT_RE = re.compile(
r"(?<![\w.])(?:inputs|secrets)\b|"
r"(?<![\w.])github\.token\b|"
r"(?<![\w.])github(?:\.token\b|\[\s*['\"]token['\"]\s*\])|"
r"(?<![\w.])github\b(?:\.event\b|\[\s*['\"]event['\"]\s*\])"
r"(?:\.(?:inputs|client_payload)\b|"
r"\[\s*['\"](?:inputs|client_payload)['\"]\s*\])|"
r"(?<![\w.])github\b(?:\.event\b|\[\s*['\"]event['\"]\s*\])?"
r"(?!\s*(?:\.|\[))|"
r"(?<![\w.])(?:steps|needs)\b"
r"(?=[^}\n]*(?:\.outputs\b|\[\s*['\"]outputs['\"]\s*\]))"
)
@@ -435,8 +443,9 @@ def audit_workflow(path: Path) -> list[Finding]:
Finding(
path,
script_index + 1,
"workflow inputs, secrets, and step/job outputs "
"must enter run scripts through env",
"workflow inputs, dispatch payloads, secrets, "
"GitHub contexts, and step/job outputs must enter "
"run scripts through env",
)
)
elif UNTRUSTED_GITHUB_CONTEXT_RE.search(expression):
+27
View File
@@ -412,6 +412,33 @@ jobs:
3,
)
def test_rejects_dispatch_data_and_whole_github_contexts_in_shell(self) -> None:
findings = self.audit(
"""permissions: {}
jobs:
unsafe:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- run: echo "${{ github.event.inputs.release_name }}"
- run: echo "${{ github.event.client_payload.command }}"
- run: echo "${{ github['event']['inputs']['release_name'] }}"
- run: echo "${{ github.event['client_payload']['command'] }}"
- run: echo "${{ toJSON(github.event) }}"
- run: echo "${{ toJSON(github) }}"
- run: echo "${{ github['token'] }}"
- env:
RELEASE_NAME: ${{ github.event.inputs.release_name }}
COMMAND: ${{ github.event.client_payload.command }}
EVENT_JSON: ${{ toJSON(github.event) }}
run: printf '%s %s %s\n' "$RELEASE_NAME" "$COMMAND" "$EVENT_JSON"
"""
)
self.assertEqual(
sum("must enter run scripts through env" in finding for finding in findings),
7,
)
def test_rejects_untrusted_github_metadata_in_generated_shell(self) -> None:
findings = self.audit(
"""on: [pull_request]