mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Block dispatch payload shell injection
Change-source: pulse-maintainer
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user