Artifact Poisoning Rule (Critical) Overview #
This rule detects unsafe artifact download practices that may allow artifact poisoning attacks. Artifact poisoning occurs when malicious artifacts from untrusted sources overwrite existing files in the runner workspace, potentially leading to code execution in privileged contexts.
Key Features: #
- Artifact Download Detection: Identifies uses of
actions/download-artifactwithout proper path isolation - Extraction Path Validation: Ensures artifacts are extracted to safe, isolated locations
- Auto-fix Support: Automatically configures safe extraction paths using
${{ runner.temp }}/artifacts - Supply Chain Protection: Prevents malicious artifacts from compromising the build environment
Security Impact #
Severity: Critical (9/10)
Artifact poisoning represents a critical security vulnerability in CI/CD pipelines:
- File Overwriting: Malicious artifacts can replace legitimate files in the workspace
- Code Execution: Overwritten scripts or binaries may be executed by subsequent steps
- Credential Theft: Modified code can exfiltrate secrets or access tokens
- Build Contamination: Compromised builds can propagate malicious code to production
This vulnerability is classified as CWE-829: Inclusion of Functionality from Untrusted Control Sphere and aligns with OWASP CI/CD Security Risk CICD-SEC-4: Poisoned Pipeline Execution (PPE).
Example Workflow #
Consider the following vulnerable workflow that downloads artifacts unsafely:
name: Deploy Application
on:
workflow_run:
workflows: ["Build"]
types: [completed]
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
# VULNERABLE: No path specified, downloads to current directory
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: application-bundle
# DANGEROUS: Executes scripts from downloaded artifacts
- name: Deploy to production
run: |
chmod +x ./deploy.sh
./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Attack Scenario #
How Artifact Poisoning Works:
- Attacker Creates Malicious PR: Opens a pull request with seemingly innocent changes
- Build Workflow Runs: CI workflow builds the PR and uploads artifacts
- Malicious Artifact Created: Attacker includes a malicious
deploy.shin the artifact - Deploy Workflow Triggered: Downstream workflow downloads the artifact
- Files Overwritten: Malicious
deploy.shoverwrites the legitimate deployment script - Code Execution: Deploy workflow executes the malicious script with production credentials
- Compromise: Attacker gains access to production environment and secrets
This attack is particularly dangerous because:
- The deploy workflow may run with write permissions or production access
- Artifacts appear to come from the same repository (trusted source)
- Traditional security scanners don’t inspect artifact contents
- The malicious code executes in a privileged context
Example Output #
Running sisakulint will detect unsafe artifact downloads:
$ sisakulint
.github/workflows/deploy.yaml:12:9: artifact is downloaded without specifying a safe extraction path at step "Download build artifacts". This may allow artifact poisoning where malicious files overwrite existing files. Consider extracting to a temporary folder like '${{ runner.temp }}/artifacts' to prevent overwriting existing files. See https://codeql.github.com/codeql-query-help/actions/actions-artifact-poisoning-critical/ [artifact-poisoning]
12 👈| - name: Download build artifacts
Auto-fix Support #
The artifact-poisoning rule supports auto-fixing by adding safe extraction paths:
# Preview changes without applying
sisakulint -fix dry-run
# Apply fixes
sisakulint -fix on
After auto-fix, artifacts are extracted to an isolated temporary directory:
name: Deploy Application
on:
workflow_run:
workflows: ["Build"]
types: [completed]
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
# SECURE: Artifacts isolated in temporary directory
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: application-bundle
path: ${{ runner.temp }}/artifacts
# SECURE: Reference files from isolated location
- name: Verify artifact contents
run: |
# Validate artifact integrity before use
sha256sum -c ${{ runner.temp }}/artifacts/checksums.txt
- name: Deploy to production
run: |
# Copy only verified files to workspace
cp ${{ runner.temp }}/artifacts/app.tar.gz .
tar -xzf app.tar.gz
chmod +x ./deploy.sh
./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Best Practices #
1. Always Specify Extraction Path #
Use runner.temp to isolate artifacts from the workspace:
- uses: actions/download-artifact@v4
with:
name: my-artifact
path: ${{ runner.temp }}/artifacts # Isolated directory
2. Treat Artifacts as Untrusted #
Never execute artifact contents directly. Always validate first:
- name: Validate artifact
run: |
# Check file signatures
gpg --verify ${{ runner.temp }}/artifacts/app.sig ${{ runner.temp }}/artifacts/app
# Verify checksums
sha256sum -c ${{ runner.temp }}/artifacts/checksums.txt
# Scan for malicious content
clamav-scan ${{ runner.temp }}/artifacts/
3. Limit Artifact Scope #
Minimize what artifacts can contain and where they’re used:
# Good: Specific artifact with limited scope
- uses: actions/download-artifact@v4
with:
name: test-results
path: ${{ runner.temp }}/test-results
# Bad: Downloading all artifacts to workspace
- uses: actions/download-artifact@v4
# No path specified - downloads all artifacts to current directory!
4. Use Separate Workflows for Privileged Operations #
Isolate workflows with production access from untrusted inputs:
# Build workflow (runs on PR, untrusted)
name: Build
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
---
# Deploy workflow (runs on main only, trusted)
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # For OIDC
steps:
- uses: actions/checkout@v4 # Get trusted code
- run: npm run build # Build from trusted source
# Don't download artifacts from PRs
- run: ./deploy.sh
5. Implement Content Security Policies #
Restrict what can be executed from downloaded artifacts:
- name: Configure security policy
run: |
# Disable execution of downloaded files
find ${{ runner.temp }}/artifacts -type f -exec chmod -x {} \;
- name: Selectively enable execution
run: |
# Only enable execution after validation
if verify-signature ${{ runner.temp }}/artifacts/deploy.sh; then
chmod +x ${{ runner.temp }}/artifacts/deploy.sh
fi
Real-World Attack Vectors #
Attack Vector 1: Pull Request Poisoning #
Scenario: Attacker creates PR with malicious artifact that gets executed in privileged workflow
# Vulnerable: PR builds upload artifacts, deploy workflow downloads them
on:
workflow_run:
workflows: ["PR Build"]
types: [completed]
Mitigation: Separate build and deploy, only deploy from trusted branches
Attack Vector 2: Dependency Chain Poisoning #
Scenario: Compromised dependency uploads malicious artifacts during build
# Vulnerable: Build process controlled by dependencies
steps:
- run: npm install # Compromised package
- run: npm run build # Malicious build script uploads poisoned artifact
Mitigation: Use dependency pinning, verify artifact integrity, scan for malware
Attack Vector 3: Cross-Workflow Contamination #
Scenario: Multiple workflows share artifact namespace, attacker poisons shared artifact
# Workflow A uploads "config" artifact
- uses: actions/upload-artifact@v4
with:
name: config
# Workflow B downloads "config" artifact (could be poisoned)
- uses: actions/download-artifact@v4
with:
name: config
Mitigation: Use unique artifact names with workflow/run IDs, validate sources
Detection Patterns #
The artifact-poisoning rule detects the following unsafe patterns:
Missing path parameter:
- uses: actions/download-artifact@v4 with: name: my-artifact # Missing: path parameterEmpty path parameter:
- uses: actions/download-artifact@v4 with: name: my-artifact path: "" # Empty pathWorkspace path parameter:
- uses: actions/download-artifact@v4 with: name: my-artifact path: . # Current directory - unsafe
Safe Patterns #
The rule recognizes these patterns as safe:
Temporary directory isolation:
- uses: actions/download-artifact@v4 with: name: my-artifact path: ${{ runner.temp }}/artifactsCustom temporary path:
- uses: actions/download-artifact@v4 with: name: my-artifact path: ${{ runner.temp }}/my-safe-location
Integration with GitHub Security Features #
This rule complements GitHub’s native security features:
- CODEOWNERS: Require review for workflow changes
- Branch Protection: Prevent direct pushes to protected branches
- Required Status Checks: Ensure sisakulint passes before merge
- Deployment Protection Rules: Require approval for production deployments
- OpenID Connect (OIDC): Use short-lived tokens instead of long-lived secrets
CodeQL Integration #
This rule is inspired by CodeQL’s artifact-poisoning query:
sisakulint provides:
- Faster feedback during development
- Auto-fix capabilities
- Integration with CI/CD pipelines
- No GitHub Advanced Security license required
OWASP CI/CD Security Alignment #
This rule addresses multiple OWASP CI/CD Security Risks:
CICD-SEC-4: Poisoned Pipeline Execution (PPE)
- Prevents malicious artifacts from executing in privileged contexts
- Isolates untrusted content from execution environment
Complementary Rules #
Use these rules together for comprehensive protection:
permissions rule: Limit workflow permissions
permissions: contents: read # Don't allow write accesstimeout-minutes rule: Prevent resource exhaustion
timeout-minutes: 10 # Limit execution timeissue-injection rule: Prevent command injection from artifacts
# Use environment variables, not direct interpolation env: ARTIFACT_NAME: ${{ github.event.inputs.name }}commit-sha rule: Pin actions to prevent supply chain attacks
- uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935
Configuration #
To customize the artifact-poisoning rule behavior, configure .github/action.yaml:
# Currently no configuration options available
# Rule always enforces safe artifact extraction paths
Performance Considerations #
This rule has minimal performance impact:
- Detection: O(n) where n is the number of steps
- Auto-fix: Modifies YAML structure in-place
- No Network Calls: Purely static analysis
See Also #
Industry References:
- CodeQL: Artifact Poisoning (Critical) - CodeQL’s artifact poisoning detection
- GitHub: Security Hardening for GitHub Actions - Official GitHub security guidance
- OWASP: CICD-SEC-04 - Poisoned Pipeline Execution - PPE attack patterns
- CWE-829: Inclusion of Functionality from Untrusted Control Sphere - Vulnerability classification
- GitHub Docs: Storing Workflow Data as Artifacts - Artifact usage guidelines
