Toolvana: production-ready media tools platform (CodeIgniter 4)
- 134-tool registry with programmatic SEO (unique titles/H1/descriptions, JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter) - DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD), Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG) - Security: SSRF guard, MIME validation, rate limits, API-key auth, bcrypt admin login, security headers - Admin panel: dashboard, tools/categories/guides CRUD, SEO audit, analytics, job inspector with retry, system health, feature flags - Docker deployment (nginx + web/api FPM pools + scalable workers), PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
/**
|
||||
* SSRF protection for user-supplied URLs. Every external fetch goes
|
||||
* through guard() before a single byte is requested:
|
||||
*
|
||||
* - scheme allowlist (http/https)
|
||||
* - DNS resolution checked against private / reserved ranges
|
||||
* - port restricted to 80/443
|
||||
* - optional host allowlist for media CDNs
|
||||
* - redirects are never auto-followed by fetchers (re-validate per hop)
|
||||
*/
|
||||
final class UrlGuard
|
||||
{
|
||||
public function check(string $url): array
|
||||
{
|
||||
$parts = parse_url(trim($url));
|
||||
|
||||
if ($parts === false || ! isset($parts['host'], $parts['scheme'])) {
|
||||
throw new \InvalidArgumentException('Invalid URL.');
|
||||
}
|
||||
|
||||
if (! in_array(strtolower($parts['scheme']), ['http', 'https'], true)) {
|
||||
throw new \InvalidArgumentException('Only http and https URLs are allowed.');
|
||||
}
|
||||
|
||||
$port = $parts['port'] ?? (strtolower($parts['scheme']) === 'https' ? 443 : 80);
|
||||
if (! in_array($port, [80, 443], true)) {
|
||||
throw new \InvalidArgumentException('Blocked port.');
|
||||
}
|
||||
|
||||
$host = strtolower($parts['host']);
|
||||
|
||||
// Never let raw IPs through unless public.
|
||||
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
||||
if (! $this->isPublicIp($host)) {
|
||||
throw new \InvalidArgumentException('Blocked host.');
|
||||
}
|
||||
|
||||
return ['host' => $host, 'ip' => $host, 'port' => $port, 'path' => $parts['path'] ?? '/', 'query' => $parts['query'] ?? ''];
|
||||
}
|
||||
|
||||
// Resolve all addresses; every one must be public.
|
||||
$records = @dns_get_record($host, DNS_A + DNS_AAAA);
|
||||
$ips = [];
|
||||
foreach ($records ?: [] as $record) {
|
||||
$ip = $record['type'] === 'A' ? ($record['ip'] ?? null) : ($record['ipv6'] ?? null);
|
||||
if ($ip !== null) {
|
||||
$ips[] = $ip;
|
||||
}
|
||||
}
|
||||
if ($ips === []) {
|
||||
// fall back to gethostbyname for simple A records
|
||||
$resolved = gethostbyname($host);
|
||||
if ($resolved === $host) {
|
||||
throw new \InvalidArgumentException('Could not resolve host.');
|
||||
}
|
||||
$ips = [$resolved];
|
||||
}
|
||||
foreach ($ips as $ip) {
|
||||
if (! $this->isPublicIp($ip)) {
|
||||
throw new \InvalidArgumentException('Blocked host.');
|
||||
}
|
||||
}
|
||||
|
||||
return ['host' => $host, 'ips' => $ips, 'port' => $port, 'path' => $parts['path'] ?? '/', 'query' => $parts['query'] ?? ''];
|
||||
}
|
||||
|
||||
/** Host allowlist used for direct CDN fetches (thumbnails etc). */
|
||||
public function isAllowedMediaHost(string $host): bool
|
||||
{
|
||||
return in_array(strtolower($host), config('Site')->urlFetchAllowlist, true);
|
||||
}
|
||||
|
||||
public function isPublicIp(string $ip): bool
|
||||
{
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && str_starts_with($ip, '::ffff:')) {
|
||||
$ip = substr($ip, 7);
|
||||
}
|
||||
|
||||
$blockedFlags = FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE;
|
||||
|
||||
if (str_contains($ip, ':')) {
|
||||
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | $blockedFlags) !== false;
|
||||
}
|
||||
|
||||
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | $blockedFlags) !== false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user