Skip to content

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

Description

@sulthonzh

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions