actions / actions/toolkit

[Security/Medium-High]: SSRF vulnerability in downloadTool() allows internal network access

Open
#2,417 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
5.9k
Forks
1.8k
PR merge metrics
No merged PRs in 30d

Description

Description

The downloadTool() function in packages/tool-cache/src/tool-cache.ts accepts arbitrary URLs without validation. This allows Server-Side Request Forgery (SSRF) attacks that can be used to:

  • Access cloud metadata services (AWS: 169.254.169.254, GCP: metadata.google.internal, Azure)
  • Scan internal networks and localhost
  • Potentially exfiltrate credentials from metadata services

Steps to Reproduce

  1. Create a GitHub Action that uses @actions/tool-cache
  2. Call downloadTool() with an internal URL:
const tc = require(@actions/tool-cache);
tc.downloadTool(http://169.254.169.254/latest/meta-data/);
tc.downloadTool(http://localhost:8080/admin);
  1. Observe that the request succeeds without blocking

Root Cause

In downloadToolAttempt() (line 80), the URL is passed directly to http.get():

const response: httpm.HttpClientResponse = await http.get(url, headers)

No validation checks:

  • URL scheme (allows http://, file://, etc.)
  • Hostname (allows localhost, private IPs, metadata services)
  • Protocol enforcement

Impact

  • Information disclosure from metadata services (IAM credentials, instance IDs)
  • Internal network scanning of GitHub Actions infrastructure
  • Localhost access for local services
  • Supply chain attacks: compromised popular actions can be weaponized

The @actions/toolkit is widely used by thousands of GitHub Actions, making this a high-value target for supply chain attacks.

Suggested Fix

Add URL validation before making HTTP requests:

const BLOCKED_PATTERNS = [
  /^169\.254\.169\.254$/,  // AWS metadata
  /^metadata\.google\.internal$/,  // GCP metadata
  /^metadata\.azure\.net$/,  // Azure metadata
  /^localhost$/,
  /^127\.0\.0\.1$/,
  /^::1$/,
  /^10\./,  // Private IP ranges
  /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
  /^192\.168\./,
  /^file:\/\//,  // Block local file access
]

function isPrivateIP(hostname: string): boolean {
  const privateRanges = [
    /^10\./,
    /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
    /^192\.168\./,
    /^fc00:/i,
    /^fd/i,
  ]
  return privateRanges.some(r => r.test(hostname))
}

async function downloadToolAttempt(url: string, dest: string, auth?: string, headers?: OutgoingHttpHeaders): Promise<string> {
  const parsedUrl = new URL(url)

  // Block localhost and private IPs
  if (parsedUrl.hostname === localhost || 
      parsedUrl.hostname === 127.0.0.1 || 
      parsedUrl.hostname === ::1 ||
      isPrivateIP(parsedUrl.hostname)) {
    throw new Error(`Blocked: cannot download from private IP: ${parsedUrl.hostname}`)
  }

  // Block metadata services
  if (parsedUrl.hostname === 169.254.169.254 || 
      parsedUrl.hostname === metadata.google.internal ||
      parsedUrl.hostname.endsWith(.metadata.azure.net)) {
    throw new Error(`Blocked: cannot access metadata service`)
  }

  // Require HTTPS (block http:// and file://)
  if (parsedUrl.protocol !== https:) {
    throw new Error(`Blocked: only HTTPS URLs allowed`)
  }

  // ... existing download logic ...
  const response: httpm.HttpClientResponse = await http.get(url, headers)
  // ...
}

Alternative: Use allowlist for known safe domains (github.com, nodejs.org, etc.).

Environment

  • Version: Latest commit (2026-05-25)
  • Package: @actions/tool-cache
  • Tested on: GitHub Actions runners (ubuntu-latest)

Additional Context

While GitHub Actions runners have network isolation, the SSRF risk is still significant:

  1. Actions often have elevated permissions (access to secrets, write access to repos)
  2. Metadata service access can expose temporary credentials
  3. This is a common pattern in supply chain attacks

Given the repository notes that contributions are not accepted, I understand this will be reviewed internally. I can provide additional testing scripts or PoCs if needed.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/tool-cache/src/tool-cache.ts at downloadToolAttempt() and trace how the URL reaches http.get(). Reproduce the reported localhost, private-address, metadata-service, and non-HTTPS cases, then verify that the chosen validation behavior blocks unsafe access without breaking intended downloads.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
devtools, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.