AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL unreviewed MIT Self-run

Sast Ssrf

skill-utkusen-sast-skills-sast-ssrf · by utkusen

>-

No reviews yet
0 installs
25 views
0.0% view→install

Install

$ agentstack add skill-utkusen-sast-skills-sast-ssrf

Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.

Security review

⚠ Flagged

1 finding(s); flagged for manual review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures
  • high Dangerous shell/eval execution.

What it can access

  • Network access Used
  • Filesystem access Used
  • Shell / process execution Used
  • Environment & secrets No
  • Dynamic code execution Used

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Reliability & compatibility

Not yet reviewed
0 installs to date
no reviews yet
4mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Sast Ssrf? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Server-Side Request Forgery (SSRF) Detection

You are performing a focused security assessment to find SSRF vulnerabilities in a codebase. This skill uses a three-phase approach with subagents: recon (find all places that make outbound TCP, DNS, or HTTP requests), batched verify (trace whether user-supplied input reaches those call sites, in parallel batches of 3), and merge (consolidate batch reports into one file).

Prerequisites: sast/architecture.md must exist. Run the analysis skill first if it doesn't.


What is SSRF

SSRF occurs when an attacker can cause the server to make outbound network requests to an arbitrary destination — including internal services, cloud metadata endpoints, or other external targets — by supplying or influencing the URL, hostname, IP, or port used in a server-side request.

The core pattern: unvalidated, user-controlled input reaches the destination argument of an outbound network call.

What SSRF IS

  • HTTP client calls where the URL or host is built from user input: requests.get(user_url)
  • Fetching a resource whose location is provided by the client: fetch(req.body.webhook_url)
  • DNS lookups on a hostname supplied by the user: dns.lookup(req.query.host)
  • Raw TCP connections to a host/port derived from user input: socket.connect((user_host, user_port))
  • File-fetching functions used with HTTP/FTP URLs from user input: file_get_contents($user_url)
  • URL redirectors that forward to a user-supplied destination without validation
  • Webhooks, import-from-URL, screenshot services, PDF renderers, image proxies — any feature that fetches a remote resource on behalf of the user

What SSRF is NOT

Do not flag these:

  • Open redirects: Redirecting the browser (HTTP 302) to a user-supplied URL — that's a client-side redirect, not a server-side request
  • XSS via URL: Rendering a user-supplied URL in an `` tag without escaping — that's XSS
  • IDOR: Accessing another user's data by changing an object ID — separate vulnerability class
  • Hardcoded outbound calls: HTTP requests to fixed, fully hardcoded URLs with no user influence — not SSRF

Patterns That Prevent SSRF

When you see these patterns, the code is likely not vulnerable:

1. Strict allowlist of permitted destinations

ALLOWED_HOSTS = {"api.example.com", "cdn.example.com"}
parsed = urlparse(user_url)
if parsed.hostname not in ALLOWED_HOSTS:
    raise ValueError("Destination not allowed")
requests.get(user_url)

2. Allowlist of permitted URL prefixes / schemes

ALLOWED_PREFIXES = ["https://api.example.com/", "https://cdn.example.com/"]
if not any(user_url.startswith(p) for p in ALLOWED_PREFIXES):
    abort(400)
requests.get(user_url)

3. No user influence on the destination

# Destination fully hardcoded — no user input involved
response = requests.get("https://api.thirdparty.com/data")

> Note: IP blocklists (blocking 169.254.0.0/16, 10.0.0.0/8, etc.) are not sufficient protection — they can be bypassed via DNS rebinding, URL encoding, IPv6 notation, decimal IP representation, or redirect chains. Do not treat a blocklist as making a site safe; classify it as Likely Vulnerable.


Vulnerable vs. Secure Examples

Python — requests

# VULNERABLE: URL fully controlled by user
@app.route('/fetch')
def fetch():
    url = request.args.get('url')
    response = requests.get(url)
    return response.text

# SECURE: strict allowlist on destination host
ALLOWED = {"api.example.com"}
@app.route('/fetch')
def fetch():
    url = request.args.get('url')
    if urlparse(url).hostname not in ALLOWED:
        abort(403)
    response = requests.get(url)
    return response.text

Python — urllib

# VULNERABLE: user controls the URL passed to urlopen
def preview(request):
    target = request.GET.get('target')
    data = urllib.request.urlopen(target).read()
    return HttpResponse(data)

# SECURE: only allow https scheme to a hardcoded host
def preview(request):
    target = request.GET.get('target')
    parsed = urlparse(target)
    if parsed.scheme != 'https' or parsed.hostname != 'media.example.com':
        return HttpResponse(status=400)
    data = urllib.request.urlopen(target).read()
    return HttpResponse(data)

Node.js — fetch / axios

// VULNERABLE: webhook URL comes directly from request body
app.post('/webhook/test', async (req, res) => {
  const { url } = req.body;
  const result = await fetch(url);
  res.json(await result.json());
});

// SECURE: allowlist check before fetch
const ALLOWED_HOSTS = new Set(['hooks.example.com']);
app.post('/webhook/test', async (req, res) => {
  const { url } = req.body;
  const { hostname } = new URL(url);
  if (!ALLOWED_HOSTS.has(hostname)) return res.status(403).send('Forbidden');
  const result = await fetch(url);
  res.json(await result.json());
});

Node.js — http.request

// VULNERABLE: host and path from query string
app.get('/proxy', (req, res) => {
  const { host, path } = req.query;
  http.get({ host, path }, (proxyRes) => proxyRes.pipe(res));
});

Ruby on Rails — Net::HTTP / OpenURI

# VULNERABLE: open() fetches arbitrary URL
def import
  url = params[:url]
  content = URI.open(url).read  # also triggers for open(url) via Kernel#open
  # ...
end

# SECURE: restrict scheme and host
def import
  url = params[:url]
  uri = URI.parse(url)
  raise "Forbidden" unless uri.is_a?(URI::HTTPS) && uri.host == "data.example.com"
  content = uri.open.read
  # ...
end

PHP — cURL

// VULNERABLE: user-supplied URL piped into curl
function fetch_preview($url) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $result = curl_exec($ch);
    curl_close($ch);
    return $result;
}
// Called as: fetch_preview($_GET['url'])

// SECURE: validate URL against allowlist before curl
function fetch_preview($url) {
    $allowed = ['https://cdn.example.com/'];
    foreach ($allowed as $prefix) {
        if (strpos($url, $prefix) === 0) {
            // ... proceed with curl
        }
    }
    throw new Exception("Destination not allowed");
}

PHP — filegetcontents

// VULNERABLE: file_get_contents with http:// wrapper and user input
$url = $_GET['source'];
$data = file_get_contents($url);  // fetches remote URL if scheme is http/https/ftp

Java — Spring / OkHttp

// VULNERABLE: RestTemplate with user-controlled URL
@GetMapping("/proxy")
public ResponseEntity proxy(@RequestParam String url) {
    RestTemplate restTemplate = new RestTemplate();
    return restTemplate.getForEntity(url, String.class);
}

// VULNERABLE: OkHttp with user-controlled host
public String fetch(String host, String path) {
    Request request = new Request.Builder()
        .url("https://" + host + path)
        .build();
    return client.newCall(request).execute().body().string();
}

Go — net/http

// VULNERABLE: user-supplied URL passed to http.Get
func proxyHandler(w http.ResponseWriter, r *http.Request) {
    target := r.URL.Query().Get("url")
    resp, err := http.Get(target)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    io.Copy(w, resp.Body)
}

// VULNERABLE: user controls host in net.Dial
func dialHandler(w http.ResponseWriter, r *http.Request) {
    host := r.URL.Query().Get("host")
    port := r.URL.Query().Get("port")
    conn, _ := net.Dial("tcp", host+":"+port)
    // ...
}

C# — HttpClient

// VULNERABLE: user-supplied URL passed to HttpClient
[HttpGet("proxy")]
public async Task Proxy([FromQuery] string url)
{
    var response = await _httpClient.GetAsync(url);
    var content = await response.Content.ReadAsStringAsync();
    return Content(content);
}

Execution

This skill runs in three phases using subagents. Pass the contents of sast/architecture.md to all subagents as context.

Phase 1: Find All Outbound Network Call Sites

Launch a subagent with the following instructions:

> Goal: Find every location in the codebase where the application makes an outbound network request — HTTP, HTTPS, FTP, TCP, or DNS — regardless of whether that destination is user-controlled. Write results to sast/ssrf-recon.md. > > Context: You will be given the project's architecture summary. Use it to understand the tech stack, HTTP client libraries in use, and any networking or webhook-related components. > > What to search for — outbound request call sites: > > You are looking for any code that opens a network connection or fetches a remote resource. Flag ANY call where a non-trivially-hardcoded URL, host, or address value is passed as an argument. You are not yet tracing whether that value is user-controlled; that is Phase 2's job. > > 1. Python HTTP clients: > - requests.get(url), requests.post(url), requests.put(url), requests.request(method, url), requests.Session().get(url) > - urllib.request.urlopen(url), urllib2.urlopen(url) > - httpx.get(url), httpx.post(url), httpx.AsyncClient().get(url) > - aiohttp.ClientSession().get(url), aiohttp.ClientSession().post(url) > > 2. Python socket / DNS: > - socket.connect((host, port)), socket.create_connection((host, port)) > - dns.resolver.resolve(name), socket.getaddrinfo(host, ...) > > 3. Python file-fetching with remote schemes: > - urllib.request.urlopen(url) where url may be http/https/ftp > - open(url) via from urllib.request import urlopen or similar (flag if url may be remote) > > 4. Node.js / JavaScript HTTP clients: > - fetch(url), node-fetch(url) > - axios.get(url), axios.post(url), axios.request({url}) > - http.get(url), https.get(url), http.request(options), https.request(options) > - got(url), superagent.get(url), needle.get(url), undici.request(url) > - require('request')(options) > > 5. Node.js socket / DNS: > - net.createConnection({host, port}), net.connect(port, host) > - dns.lookup(hostname, ...), dns.resolve(hostname, ...), dns.resolve4(hostname) > > 6. Ruby HTTP clients: > - Net::HTTP.get(uri), Net::HTTP.start(host, ...), Net::HTTP.get_response(url) > - URI.open(url), open(url) (Kernel#open / OpenURI) > - RestClient.get(url), RestClient::Resource.new(url) > - Faraday.new(url).get(path), HTTParty.get(url) > - Typhoeus::Request.new(url) > > 7. PHP HTTP clients and file functions: > - curl_setopt($ch, CURLOPT_URL, $url) followed by curl_exec($ch) > - file_get_contents($url) — flag when $url may be an http/https/ftp URL > - fopen($url, 'r') with a remote URL scheme > - Guzzle: $client->request('GET', $url), $client->get($url) > - Symfony HttpClient: $client->request('GET', $url) > > 8. Java HTTP clients: > - new URL(url).openConnection(), new URL(url).openStream() > - HttpURLConnection / HttpsURLConnection with a dynamic URL > - OkHttpClient().newCall(new Request.Builder().url(url)...) > - RestTemplate.getForObject(url, ...), RestTemplate.getForEntity(url, ...) > - WebClient.get().uri(url), WebClient.create(url) > - Apache HttpClient: httpClient.execute(new HttpGet(url)) > > 9. Go HTTP clients and network dials: > - http.Get(url), http.Post(url, ...), http.NewRequest("GET", url, ...) > - net.Dial("tcp", addr), net.DialTCP(...), net.DialTimeout("tcp", addr, ...) > - net.LookupHost(hostname), net.LookupAddr(addr), net.ResolveIPAddr(...) > - net.ResolveTCPAddr("tcp", addr) > > 10. C# / .NET HTTP clients: > - HttpClient.GetAsync(url), HttpClient.PostAsync(url, ...), HttpClient.SendAsync(request) > - WebRequest.Create(url), WebClient.DownloadString(url), WebClient.DownloadData(url) > - HttpWebRequest with a dynamic URL > > 11. Shell-out to network tools (via subprocess, exec, system, etc.): > - subprocess.run(["curl", url, ...]), subprocess.Popen(["wget", url, ...]) > - os.system("curl " + url), exec("wget " + url) > - Any curl, wget, nc, ncat, nmap invocation where the target is a variable > > What to skip (these are safe — do not flag): > - Calls where the entire URL and hostname are fully hardcoded string literals with no dynamic parts: requests.get("https://api.example.com/data") > - Internal loopback connections to localhost or 127.0.0.1 that are clearly part of service-to-service architecture (e.g., connecting to a local queue) — flag these if the address is dynamic > > Output format — write to sast/ssrf-recon.md: > > ``markdown > # SSRF Recon: [Project Name] > > ## Summary > Found [N] outbound network call sites. > > ## Outbound Call Sites > > ### 1. [Descriptive name — e.g., "HTTP GET in webhook dispatcher"] > - **File**: path/to/file.ext (lines X-Y) > - **Function / endpoint**: [function name or route] > - **Call type**: [HTTP GET / HTTP POST / TCP dial / DNS lookup / subprocess curl / etc.] > - **Library / method**: [requests.get / fetch / http.Get / curl_exec / etc.] > - **Destination argument**: varname or urlexpression — [brief note, e.g., "assembled from query param" or "partially hardcoded path with variable host"] > - **Code snippet**: > ` > [the outbound call and the lines immediately before it that construct the destination] > ` > > [Repeat for each site] > ``

After Phase 1: Check for Candidates Before Proceeding

After Phase 1 completes, read sast/ssrf-recon.md. If the recon found zero outbound call sites (the summary reports "Found 0" or the "Outbound Call Sites" section is empty or absent), skip Phase 2 and Phase 3 entirely. Instead, write the following content to sast/ssrf-results.md and stop:

# SSRF Analysis Results

No vulnerabilities found.

Only proceed to Phase 2 if Phase 1 found at least one outbound call site.

Phase 2: Verify — Trace User Input to Outbound Call Sites (Batched)

After Phase 1 completes, read sast/ssrf-recon.md and split the outbound call sites into batches of up to 3 sites each. Launch one subagent per batch in parallel. Each subagent traces taint only for its assigned sites and writes results to its own batch file.

Batching procedure (you, the orchestrator, do this — not a subagent):

  1. Read sast/ssrf-recon.md and count the numbered site sections (### 1., ### 2., etc.) under "Outbound Call Sites".
  2. Divide them into batches of up to 3. For example, 8 sites → 3 batches (1-3, 4-6, 7-8).
  3. For each batch, extract the full text of those site sections from the recon file.
  4. Launch all batch subagents in parallel, passing each one only its assigned sites.
  5. Each subagent writes to sast/ssrf-batch-N.md where N is the 1-based batch number.
  6. Identify the project's primary language/framework from sast/architecture.md and select only the matching examples from the "Vulnerable vs. Secure Examples" section above. For example, if the project uses Node.js with fetch/axios, include only the "Node.js — fetch / axios" and "Node.js — http.request" examples. Include these selected examples in each subagent's instructions where indicated by [TECH-STACK EXAMPLES] below.

Give each batch subagent the following instructions (substitute the batch-specific values):

> Goal: For each assigned outbound network call site, determine whether a user-supplied value controls or influences the destination (URL, host, path, port, or scheme). Our goal is to find SSRF vulnerabilities. Write results to sast/ssrf-batch-[N].md. > > Your assigned outbound call sites (from the recon phase): > > [Paste the full text of the assigned site sections here, preserving the original numbering] > > Context: You will be given the project's architecture summary. Use i

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.