ssrf

A security rule for finding server-side request forgery, or SSRF, in Forge apps. SSRF happens when user-controlled input makes an app send requests to unintended internal or external addresses.

In plain words
What is it for?
Reviewing Forge code that uses requestJira, requestConfluence, or fetch, especially user-controlled URLs, paths, and parameters.
Why use it?
It highlights unsafe URL construction and missing validation in outbound requests, which can let attackers reach protected services.

Cursor rule

Install

Getting it into your agent

One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.

agentmods
npx agentmods add rules/atlassian/forge-skills/ssrf
Clone the repo
git clone --depth 1 https://github.com/atlassian/forge-skills
Per session 18 Only the description is in the session, so the agent can decide to use it. The body loads when it is invoked.
When invoked 1,232 The whole file, excluding the scripts and references it only reads on demand.
Security scan C 2 findings. Scan, not verified.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5 $0.00018 $0.01232
Opus 5 $0.00009 $0.00616
Sonnet 5 $0.00004 $0.00246
Haiku 4.5 $0.00002 $0.00123

Measured 2d ago against content hash 4caac3b26669, method: parsed. Prices are Anthropic first-party input rates as of 2026-08-30, from the pricing page.

Security

Grade C, and why

ssrf scanned grade C with 2 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 2d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Cloud metadata endpointhighServer-side request forgery

One request to 169.254.169.254 can return temporary IAM credentials.

- Cloud metadata: http://169.254.169.254/

Makes network callslowCapability

Not a fault in itself. Listed so you know the mod talks to something, and to what.

await fetch(webhookUrl); // SSRF to internal services
skills/forge-security-review/assets/security-rules/forge-injection/ssrf.mdc · 160 lines

How it starts

The opening of the file, as written. The whole thing — 160 lines — stays where its author put it; the contents beside it link to each section on GitHub.

Context

  • Forge apps can make outbound HTTP requests via requestJira, requestConfluence, fetch, and remote backends. SSRF occurs when user input controls request destinations.
  • Related CWE: CWE-918 (Server-Side Request Forgery).
  • Forge-specific: The route template literal is designed to prevent SSRF; bypassing it is a key vulnerability.

Scope & Signals

  • Safe pattern: route template literal with validated parameters.
  • Dangerous patterns:
    • String concatenation in API routes.
    • User input in fetch() URLs.
    • Dynamic URL construction without validation.
    • Missing route usage with requestJira/requestConfluence.

Vulnerable Patterns

// VULNERABLE - String concatenation instead of route
const issueKey = payload.issueKey;  // User controlled
await api.requestJira(`/rest/api/3/issue/${issueKey}/comment`);
// Attacker: issueKey = "../../../admin/sensitive"

// VULNERABLE - Direct URL from user input
const webhookUrl = payload.callbackUrl;  // User controlled
await fetch(webhookUrl);  // SSRF to internal services

// VULNERABLE - User controls path segment
const endpoint = payload.endpoint;  // User controlled
await api.requestJira(route`/rest/api/3/${endpoint}`);
// Less obvious but still risky if endpoint can contain ../

// VULNERABLE - User input in fetch without validation
const imageUrl = payload.avatarUrl;
const response = await fetch(imageUrl);  // Could be internal URL

// VULNERABLE - Building URL from parts
const host = payload.host;
const path = payload.path;
await fetch(`https://${host}${path}`);  // Host/path injection

Secure Patterns

// SECURE - Using route template literal correctly
import { route } from '@forge/api';

const issueKey = payload.issueKey;
// Route escapes/validates the parameter
await api.requestJira(route`/rest/api/3/issue/${issueKey}/comment`);

// SECURE - Validating before use
const issueKey = payload.issueKey;
if (!/^[A-Z]+-\d+$/.test(issueKey)) {
  throw new Error('Invalid issue key format');
}
await api.requestJira(route`/rest/api/3/issue/${issueKey}`);

// SECURE - Allowlist for external URLs
const ALLOWED_HOSTS = ['api.trusted-service.com', 'webhooks.example.com'];

function validateUrl(url) {
  const parsed = new URL(url);
  if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
    throw new Error('URL not in allowlist');
  }
  if (parsed.protocol !== 'https:') {
    throw new Error('HTTPS required');
  }
  return url;
}

const webhookUrl = validateUrl(payload.callbackUrl);
await fetch(webhookUrl);

// SECURE - Using predefined endpoints only
const ENDPOINTS = {
  'issues': '/rest/api/3/issue',
  'projects': '/rest/api/3/project'
};

const endpoint = ENDPOINTS[payload.endpointName];
if (!endpoint) {
  throw new Error('Unknown endpoint');
}
await api.requestJira(route`${endpoint}`);

Detection Checklist

  • Search for requestJira, requestConfluence without route template.
  • Find fetch() calls with dynamic URLs.
  • Check for string concatenation in API paths.
  • Trace URL components to user input sources.
  • Verify external URLs are validated against allowlist.
  • Look for URL construction from multiple user inputs.

SSRF Targets in Atlassian Context

# Internal services attackers may target:
- Cloud metadata: http://169.254.169.254/
- Internal APIs: http://localhost:*/
- Other tenants: Different cloudId endpoints
- Admin endpoints: /rest/api/3/configuration

Route Template Protection

// The route`` template literal provides protection:
import { route } from '@forge/api';

// SAFE - route encodes special characters
const userInput = "../admin";
route`/rest/api/3/issue/${userInput}`
// Results in: /rest/api/3/issue/..%2Fadmin (encoded)

// BYPASS ATTEMPT - still safe
const userInput = "TEST-1/../../admin";
route`/rest/api/3/issue/${userInput}`
// Results in properly encoded path

// NOT USING route - VULNERABLE
const path = `/rest/api/3/issue/${userInput}`;  // Path traversal works!

PoC / Test Leads

Read the full file on GitHub · 160 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 2d ago First seen · 160 lines · 18 tokens per session scan C 4caac3b26669

Subscribe to this mod's changes

ssrf is a cursor rule published in the GitHub repository atlassian/forge-skills (20 stars, last pushed 2d ago), licensed Apache-2.0. It adds 18 tokens to every session and 1,232 once invoked, about $0.0001 per session on Opus 5. A static security scan graded it C with 2 findings (cloud metadata endpoint, makes network calls). No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-30.