rastray 0.15.0

Blazing-fast static analysis CLI for security, dependency, and performance audits.
# RSTR-SSRF-003 — Python `requests` / `urlopen` with request input

## Summary

Python equivalent of [`RSTR-SSRF-001`](./RSTR-SSRF-001.md). The
application performs a server-side HTTP call where the URL is sourced
directly from request attributes.

## Severity

`High`. In cloud workloads, an attacker can read the IAM-role metadata
endpoint and pivot to full account compromise.

## Languages

Python.

## What rastray flags

`requests.get` / `requests.post` / `requests.put` / `requests.patch` /
`requests.delete` / `requests.head` / `requests.options` / `requests.request`,
plus `urllib.request.urlopen`, when the URL is `request.args[...]`,
`request.form[...]`, `request.json[...]`, `request.values[...]`,
`request.GET[...]`, `request.POST[...]`, or `request.data[...]`:

```python
@app.route('/proxy')
def proxy():
    return requests.get(request.args['url']).text   # ← flagged
```

```python
def view(request):
    body = urlopen(request.GET['next']).read()      # ← flagged
```

## What rastray deliberately does *not* flag

- Literal URLs.
- URLs assigned to a local first, then passed to `requests` — same
  one-step taint scope as the rest of rastray.
- `requests.Session()` configured with a fixed `base_url` (rare in
  practice but appears in some clients).

## How to fix it

Validate the target host before making the outbound call:

```python
from urllib.parse import urlparse
import ipaddress, socket

ALLOWED_HOSTS = {'api.example.com', 'cdn.example.com'}

def fetch_safe(url: str) -> bytes:
    parsed = urlparse(url)
    if parsed.scheme not in {'http', 'https'}:
        raise ValueError('only http(s) allowed')
    if parsed.hostname not in ALLOWED_HOSTS:
        raise ValueError('host not allow-listed')
    # belt-and-suspenders: resolve and reject private IPs
    ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
    if ip.is_private or ip.is_loopback or ip.is_link_local:
        raise ValueError('private destination')
    return requests.get(url, timeout=5, allow_redirects=False).content
```

`allow_redirects=False` matters — without it, an allow-listed host can
redirect the request into the metadata endpoint.

## References

- [OWASP SSRF Prevention Cheat Sheet]https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
- [Capital One SSRF write-up]https://krebsonsecurity.com/2019/08/what-we-can-learn-from-the-capital-one-hack/
- [CWE-918]https://cwe.mitre.org/data/definitions/918.html