[Security/Medium-High]: SSRF vulnerability in downloadTool() allows internal network access
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
- Create a GitHub Action that uses @actions/tool-cache
- 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);
- 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:
- Actions often have elevated permissions (access to secrets, write access to repos)
- Metadata service access can expose temporary credentials
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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