check-ssrf
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSSRF (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 resourcesphp
// 严重:包含外部引用的SVG
// 用户上传的SVG包含:
// <image xlink:href="http://internal/secret" />
// <use xlink:href="http://internal/api" />
$svg = file_get_contents($uploadedFile);
// 渲染SVG时可能会获取外部资源Grep Patterns
Grep匹配规则
bash
undefinedbash
undefinedUser 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"
undefinedGrep: "\$_(GET|POST|REQUEST)[.url" -i --glob "**/.php"
undefinedValidation 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
严重程度分类
| Pattern | Severity | OWASP |
|---|---|---|
| Cloud metadata access | 🔴 Critical | A10 |
| Internal network access | 🔴 Critical | A10 |
| User URL without validation | 🔴 Critical | A10 |
| File/gopher protocol | 🔴 Critical | A10 |
| Webhook URL unvalidated | 🟠 Major | A10 |
| Missing redirect validation | 🟠 Major | A10 |
| Protocol not restricted | 🟡 Minor | A10 |
| 模式 | 严重程度 | OWASP |
|---|---|---|
| 云元数据访问 | 🔴 严重 | A10 |
| 内部网络访问 | 🔴 严重 | A10 |
| 未验证的用户URL | 🔴 严重 | A10 |
| File/Gopher协议 | 🔴 严重 | A10 |
| 未验证的Webhook URL | 🟠 主要 | A10 |
| 缺少重定向验证 | 🟠 主要 | A10 |
| 未限制协议 | 🟡 次要 | A10 |
Output Format
输出格式
markdown
undefinedmarkdown
undefinedSSRF: [Description]
SSRF: [描述]
Severity: 🔴 Critical
Location:
CWE: CWE-918 (Server-Side Request Forgery)
file.php:lineIssue:
User-controlled URL is fetched without validation, allowing access to internal services.
Attack Vector:
- Attacker provides URL:
http://169.254.169.254/latest/meta-data/ - Server fetches AWS credentials from metadata service
- 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严重程度: 🔴 严重
位置:
CWE: CWE-918 (服务器端请求伪造)
file.php:line问题:
用户可控的URL未经验证就被获取,允许访问内部服务。
攻击向量:
- 攻击者提供URL:
http://169.254.169.254/latest/meta-data/ - 服务器从元数据服务获取AWS凭证
- 攻击者获取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