check-ssrf

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SSRF (Server-Side Request Forgery) Security Check

SSRF(服务器端请求伪造)安全检查

Analyze PHP code for SSRF vulnerabilities (OWASP A10:2021).
分析PHP代码中的SSRF漏洞(OWASP A10:2021)。

Detection Patterns

检测模式

1. User-Controlled URLs

1. 用户可控的URL

php
// CRITICAL: Direct URL from user input
$url = $_GET['url'];
$content = file_get_contents($url);

// CRITICAL: Request URL from parameter
$response = $httpClient->get($request->input('callback'));

// CRITICAL: User input in cURL
$ch = curl_init($_POST['endpoint']);
curl_exec($ch);

// CRITICAL: Guzzle with user input
$client = new GuzzleHttp\Client();
$client->request('GET', $userProvidedUrl);
php
// 严重:直接使用用户输入的URL
$url = $_GET['url'];
$content = file_get_contents($url);

// 严重:从参数获取请求URL
$response = $httpClient->get($request->input('callback'));

// 严重:用户输入用于cURL
$ch = curl_init($_POST['endpoint']);
curl_exec($ch);

// 严重:Guzzle使用用户输入
$client = new GuzzleHttp\Client();
$client->request('GET', $userProvidedUrl);

2. Cloud Metadata Endpoint Access

2. 云元数据端点访问

php
// CRITICAL: AWS metadata endpoint
$url = 'http://169.254.169.254/latest/meta-data/iam/security-credentials/';
// User could redirect to this endpoint

// CRITICAL: GCP metadata
$url = 'http://metadata.google.internal/computeMetadata/v1/';

// CRITICAL: Azure metadata
$url = 'http://169.254.169.254/metadata/instance';

// Detection: URLs that could reach metadata
if (strpos($url, '169.254.') !== false) { /* Block */ }
php
// 严重:AWS元数据端点
$url = 'http://169.254.169.254/latest/meta-data/iam/security-credentials/';
// 用户可重定向到此端点

// 严重:GCP元数据
$url = 'http://metadata.google.internal/computeMetadata/v1/';

// 严重:Azure元数据
$url = 'http://169.254.169.254/metadata/instance';

// 检测:可能访问元数据的URL
if (strpos($url, '169.254.') !== false) { /* 拦截 */ }

3. Internal Network Access

3. 内部网络访问

php
// CRITICAL: Access to internal services
$response = file_get_contents("http://internal-api:8080/admin");

// CRITICAL: Localhost bypass
$url = $_GET['url'];
// User inputs: http://localhost/admin, http://127.0.0.1/admin
// Or: http://0.0.0.0/, http://[::1]/, http://127.1/

// CRITICAL: Private IP ranges
// 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
$url = 'http://10.0.0.1/internal-service';
php
// 严重:访问内部服务
$response = file_get_contents("http://internal-api:8080/admin");

// 严重:本地主机绕过
$url = $_GET['url'];
// 用户输入:http://localhost/admin, http://127.0.0.1/admin
// 或者:http://0.0.0.0/, http://[::1]/, http://127.1/

// 严重:私有IP段
// 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
$url = 'http://10.0.0.1/internal-service';

4. URL Parsing Bypass

4. URL解析绕过

php
// CRITICAL: Protocol confusion
$url = 'http://evil.com@internal-server/';
$url = 'http://internal-server#@evil.com/';
$url = 'http://internal-server\@evil.com/';

// CRITICAL: URL encoding bypass
$url = 'http://127.0.0.1%00.evil.com/';
$url = 'http://127。0。0。1/'; // Unicode dots

// CRITICAL: DNS rebinding - domain resolves to internal IP
$url = 'http://rebind.attacker.com/'; // First resolves to public, then to 127.0.0.1

// CRITICAL: Redirect chains
$url = 'http://allowed.com/redirect?to=http://internal/';
php
// 严重:协议混淆
$url = 'http://evil.com@internal-server/';
$url = 'http://internal-server#@evil.com/';
$url = 'http://internal-server\@evil.com/';

// 严重:URL编码绕过
$url = 'http://127.0.0.1%00.evil.com/';
$url = 'http://127。0。0。1/'; // Unicode点

// 严重:DNS重绑定 - 域名解析到内部IP
$url = 'http://rebind.attacker.com/'; // 先解析到公网,再解析到127.0.0.1

// 严重:重定向链
$url = 'http://allowed.com/redirect?to=http://internal/';

5. Protocol Attacks

5. 协议攻击

php
// CRITICAL: File protocol
$url = 'file:///etc/passwd';
$content = file_get_contents($url);

// CRITICAL: Gopher protocol (can access Redis, memcached)
$url = 'gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall';

// CRITICAL: Dict protocol
$url = 'dict://127.0.0.1:6379/info';

// CRITICAL: LDAP protocol
$url = 'ldap://evil.com/o=evil';
php
// 严重:File协议
$url = 'file:///etc/passwd';
$content = file_get_contents($url);

// 严重:Gopher协议(可访问Redis、memcached)
$url = 'gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall';

// 严重:Dict协议
$url = 'dict://127.0.0.1:6379/info';

// 严重:LDAP协议
$url = 'ldap://evil.com/o=evil';

6. PDF/Image Generation SSRF

6. PDF/图片生成中的SSRF

php
// CRITICAL: HTML to PDF with user content
$html = '<img src="' . $userUrl . '">';
$pdf->loadHtml($html);

// CRITICAL: Image URL in PDF
$pdf->image($request->input('logo_url'));

// CRITICAL: wkhtmltopdf with user HTML
shell_exec("wkhtmltopdf '$userHtml' output.pdf");
php
// 严重:使用用户内容生成HTML转PDF
$html = '<img src="' . $userUrl . '">';
$pdf->loadHtml($html);

// 严重:PDF中的图片URL
$pdf->image($request->input('logo_url'));

// 严重:wkhtmltopdf处理用户HTML
shell_exec("wkhtmltopdf '$userHtml' output.pdf");

7. Webhook/Callback SSRF

7. Webhook/回调中的SSRF

php
// CRITICAL: User-provided webhook URL
$webhookUrl = $request->input('webhook_url');
$httpClient->post($webhookUrl, ['json' => $data]);

// CRITICAL: OAuth callback
$callbackUrl = $request->input('redirect_uri');
return redirect($callbackUrl . '?code=' . $code);

// CRITICAL: Import from URL
$data = file_get_contents($request->input('import_url'));
$this->importData($data);
php
// 严重:用户提供的Webhook URL
$webhookUrl = $request->input('webhook_url');
$httpClient->post($webhookUrl, ['json' => $data]);

// 严重:OAuth回调
$callbackUrl = $request->input('redirect_uri');
return redirect($callbackUrl . '?code=' . $code);

// 严重:从URL导入数据
$data = file_get_contents($request->input('import_url'));
$this->importData($data);

8. SVG/XML External References

8. SVG/XML外部引用

php
// CRITICAL: SVG with external references
// User uploads SVG containing:
// <image xlink:href="http://internal/secret" />
// <use xlink:href="http://internal/api" />

$svg = file_get_contents($uploadedFile);
// Rendering SVG may fetch external resources
php
// 严重:包含外部引用的SVG
// 用户上传的SVG包含:
// <image xlink:href="http://internal/secret" />
// <use xlink:href="http://internal/api" />

$svg = file_get_contents($uploadedFile);
// 渲染SVG时可能会获取外部资源

Grep Patterns

Grep匹配规则

bash
undefined
bash
undefined

User URL in HTTP functions

HTTP函数中的用户URL

Grep: "(file_get_contents|fopen|curl_init|readfile)\s*([^)]\$" --glob "**/.php"
Grep: "(file_get_contents|fopen|curl_init|readfile)\s*([^)]\$" --glob "**/.php"

HTTP client with variable

带变量的HTTP客户端调用

Grep: "(->get|->post|->request)\s*([^)]\$" --glob "**/.php"
Grep: "(->get|->post|->request)\s*([^)]\$" --glob "**/.php"

Webhook/callback patterns

Webhook/回调相关模式

Grep: "(webhook|callback|redirect).url.\$" -i --glob "**/*.php"
Grep: "(webhook|callback|redirect).url.\$" -i --glob "**/*.php"

URL from request

来自请求的URL

Grep: "\$_(GET|POST|REQUEST)[.url" -i --glob "**/.php"
undefined
Grep: "\$_(GET|POST|REQUEST)[.url" -i --glob "**/.php"
undefined

Validation Patterns

验证模式

URL Allowlist

URL白名单

php
// SECURE: Strict allowlist
final class UrlValidator
{
    private const ALLOWED_HOSTS = [
        'api.trusted-service.com',
        'cdn.example.com',
    ];

    public function validate(string $url): bool
    {
        $parsed = parse_url($url);
        if ($parsed === false || !isset($parsed['host'])) {
            return false;
        }

        return in_array($parsed['host'], self::ALLOWED_HOSTS, true);
    }
}
php
// 安全:严格的白名单
final class UrlValidator
{
    private const ALLOWED_HOSTS = [
        'api.trusted-service.com',
        'cdn.example.com',
    ];

    public function validate(string $url): bool
    {
        $parsed = parse_url($url);
        if ($parsed === false || !isset($parsed['host'])) {
            return false;
        }

        return in_array($parsed['host'], self::ALLOWED_HOSTS, true);
    }
}

Block Internal Networks

拦截内部网络

php
// SECURE: Block private/internal IPs
final class SafeUrlFetcher
{
    public function fetch(string $url): string
    {
        $parsed = parse_url($url);
        $ip = gethostbyname($parsed['host']);

        if ($this->isPrivateIp($ip) || $this->isMetadataIp($ip)) {
            throw new SecurityException('Internal URL not allowed');
        }

        return file_get_contents($url);
    }

    private function isPrivateIp(string $ip): bool
    {
        return filter_var(
            $ip,
            FILTER_VALIDATE_IP,
            FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
        ) === false;
    }

    private function isMetadataIp(string $ip): bool
    {
        return str_starts_with($ip, '169.254.');
    }
}
php
// 安全:拦截私有/内部IP
final class SafeUrlFetcher
{
    public function fetch(string $url): string
    {
        $parsed = parse_url($url);
        $ip = gethostbyname($parsed['host']);

        if ($this->isPrivateIp($ip) || $this->isMetadataIp($ip)) {
            throw new SecurityException('不允许访问内部URL');
        }

        return file_get_contents($url);
    }

    private function isPrivateIp(string $ip): bool
    {
        return filter_var(
            $ip,
            FILTER_VALIDATE_IP,
            FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
        ) === false;
    }

    private function isMetadataIp(string $ip): bool
    {
        return str_starts_with($ip, '169.254.');
    }
}

Protocol Allowlist

协议白名单

php
// SECURE: Only allow HTTPS
final class SecureUrlValidator
{
    public function validate(string $url): bool
    {
        $parsed = parse_url($url);

        // Only HTTPS allowed
        if (($parsed['scheme'] ?? '') !== 'https') {
            return false;
        }

        // No credentials in URL
        if (isset($parsed['user']) || isset($parsed['pass'])) {
            return false;
        }

        return true;
    }
}
php
// 安全:仅允许HTTPS
final class SecureUrlValidator
{
    public function validate(string $url): bool
    {
        $parsed = parse_url($url);

        // 仅允许HTTPS
        if (($parsed['scheme'] ?? '') !== 'https') {
            return false;
        }

        // URL中不允许包含凭证
        if (isset($parsed['user']) || isset($parsed['pass'])) {
            return false;
        }

        return true;
    }
}

Disable Redirects

禁用重定向

php
// SECURE: Prevent redirect-based SSRF
$client = new GuzzleHttp\Client([
    'allow_redirects' => false,
    // Or limit redirects and verify each
    'allow_redirects' => [
        'max' => 3,
        'on_redirect' => function ($request, $response, $uri) {
            if (!$this->isAllowedHost($uri->getHost())) {
                throw new SecurityException('Redirect to disallowed host');
            }
        },
    ],
]);
php
// 安全:防止基于重定向的SSRF
$client = new GuzzleHttp\Client([
    'allow_redirects' => false,
    // 或者限制重定向次数并验证每个重定向
    'allow_redirects' => [
        'max' => 3,
        'on_redirect' => function ($request, $response, $uri) {
            if (!$this->isAllowedHost($uri->getHost())) {
                throw new SecurityException('不允许重定向到未授权主机');
            }
        },
    ],
]);

Severity Classification

严重程度分类

PatternSeverityOWASP
Cloud metadata access🔴 CriticalA10
Internal network access🔴 CriticalA10
User URL without validation🔴 CriticalA10
File/gopher protocol🔴 CriticalA10
Webhook URL unvalidated🟠 MajorA10
Missing redirect validation🟠 MajorA10
Protocol not restricted🟡 MinorA10
模式严重程度OWASP
云元数据访问🔴 严重A10
内部网络访问🔴 严重A10
未验证的用户URL🔴 严重A10
File/Gopher协议🔴 严重A10
未验证的Webhook URL🟠 主要A10
缺少重定向验证🟠 主要A10
未限制协议🟡 次要A10

Output Format

输出格式

markdown
undefined
markdown
undefined

SSRF: [Description]

SSRF: [描述]

Severity: 🔴 Critical Location:
file.php:line
CWE: CWE-918 (Server-Side Request Forgery)
Issue: User-controlled URL is fetched without validation, allowing access to internal services.
Attack Vector:
  1. Attacker provides URL:
    http://169.254.169.254/latest/meta-data/
  2. Server fetches AWS credentials from metadata service
  3. Attacker receives IAM credentials
Code:
php
// Vulnerable
$data = file_get_contents($_GET['url']);
Fix:
php
// Secure: Validate URL before fetching
if (!$this->urlValidator->isAllowed($url)) {
    throw new SecurityException('URL not allowed');
}
$data = file_get_contents($url);
References:
undefined
严重程度: 🔴 严重 位置:
file.php:line
CWE: CWE-918 (服务器端请求伪造)
问题: 用户可控的URL未经验证就被获取,允许访问内部服务。
攻击向量:
  1. 攻击者提供URL:
    http://169.254.169.254/latest/meta-data/
  2. 服务器从元数据服务获取AWS凭证
  3. 攻击者获取IAM凭证
代码:
php
// 存在漏洞
$data = file_get_contents($_GET['url']);
修复方案:
php
// 安全:获取URL前先验证
if (!$this->urlValidator->isAllowed($url)) {
    throw new SecurityException('URL不被允许');
}
$data = file_get_contents($url);
参考资料:
undefined