URL adresy a blokované siete
Kanonizácia
Každú URL adresu parsuje ochrana sama, nikdy vlastný parser URL v PHP: ten a curl čítajú niektorých hostiteľov rôzne (http://a.com\@127.0.0.1/, http://127.0.0.1#@a.com/), takže ochrana by overila jedného hostiteľa a curl by vytočil iného. Všetko, čo by parser mohol prečítať dvoma spôsobmi, sa odmietne a požiadavka sa pošle na URL prestavanú z overených častí. V tomto poradí, rozhoduje prvé zlyhanie:
- Riadiaci znak C0 alebo DEL kdekoľvek → invalid_url.
- Chýba scheme: → invalid_url; iná schéma než http alebo https → scheme.
- Chýba // alebo je autorita prázdna → invalid_url.
- Medzera, \, %, Unicode medzera alebo U+FEFF v autorite → non_canonical_host (hostiteľ kódovaný percentami, IPv6 zone id).
- @ v autorite → credentials.
- Port, ktorý nie sú číslice → invalid_url; mimo 1–65535 → port.
- Hostiteľ: IPv6 adresa v hranatých zátvorkách musí byť platná a skráti sa ([0:0:0:0:0:0:0:1] → [::1]); ne-ASCII alebo xn-- časť prejde cez UTS #46 (vyžaduje ext-intl); malé písmená; jedna koncová bodka sa vynechá; hostiteľ, ktorého posledná časť je číselná alebo hexadecimálna, musí mať presne štyri desiatkové časti 0–255 bez úvodných núl, napísané práve tak — inak non_canonical_host.
- \ v ceste, query alebo fragmente → invalid_url.
Fragment sa vynechá. Výsledkom je CanonicalUrl — toString() vráti schému a hostiteľa malými písmenami, bez používateľských údajov, fragmentu a predvoleného portu, IPv6 skrátenú v zátvorkách, cestu a query bez zmeny (prázdne → /); origin() vráti scheme://host:port s vždy uvedeným portom. Pravidlo schémy sa uplatní až potom, takže kanonická URL http:// sa naparsuje v poriadku a odmietne sa (scheme) pri overení.
Tabuľka blokovaných sietí
| Rozsah | Čo |
|---|---|
0.0.0.0/8 | sieť „this“ |
10.0.0.0/8 · 172.16.0.0/12 · 192.168.0.0/16 | privátne |
100.64.0.0/10 | carrier-grade NAT (metadáta Alibaba Cloud na 100.100.100.200) |
127.0.0.0/8 | loopback |
168.63.129.16/32 | Azure WireServer (verejný adresný priestor, dostupný z každého Azure VM a uzla AKS) |
169.254.0.0/16 | link-local: metadáta AWS, GCP, Azure, OpenStack na 169.254.169.254 |
192.0.0.0/24 | pridelenia protokolov IETF (metadáta Oracle Cloud na 192.0.0.192) |
192.0.2.0/24 · 198.51.100.0/24 · 203.0.113.0/24 | dokumentácia |
192.88.99.0/24 | 6to4 relay anycast |
198.18.0.0/15 | benchmarking |
224.0.0.0/4 | multicast |
240.0.0.0/4 | rezervované, broadcast |
64:ff9b:1::/48 | NAT64, lokálne použitie |
100::/64 | discard |
2001::/23 | pridelenia protokolov IETF (vrátane Teredo) |
2001:db8::/32 · 3fff::/20 | dokumentácia |
5f00::/16 | SRv6 SID |
fc00::/7 | unique local (metadáta AWS cez IPv6 na fd00:ec2::254) |
fe80::/10 | link-local |
fec0::/10 | site-local |
ff00::/8 | multicast |
- IPv6 adresa, ktorá nesie IPv4 adresu, sa posudzuje podľa tejto IPv4 adresy — podľa prvej zhody z ::ffff:0:0/96 (mapped), ::/96 (compatible, vrátane :: a ::1), 64:ff9b::/96 (NAT64), ::ffff:0:0:0/96 (translated) a 2002::/16 (6to4). [::ffff:127.0.0.1] a [2002:a9fe:a9fe::] sú preto blokované a 64:ff9b::808:808 (NAT64 pre 8.8.8.8) je verejná.
- localhost a každý *.localhost sa odmietnu podľa mena ešte pred prekladom.
- Všetko nečitateľné sa považuje za privátne. IPv6 zone id pri adrese sa pri posudzovaní ignoruje — v URL sa odmietne (%).
PrivateNetworks
RoundlyConsulting\SafeFetch\Support\PrivateNetworks obsahuje tabuľku — verejnú, čistú a statickú, bez konfigurácie a bez prekladu. Pravidlá na ňu uplatňuje ochrana; SafeFetch::networks() obaľuje isPrivate(), isBlockedHostname() a BLOCKED pre kód, ktorý uprednostňuje fasádu:
| Člen | Čo |
|---|---|
BLOCKED | list<string> — 26 rozsahov vyššie. |
IPV4_EMBEDDING | 5 IPv6 prefixov, ktoré nesú IPv4 adresu, a bajt, na ktorom začína. |
METADATA | 6 cloudových metadátových rozsahov a adries. |
BLOCKED_HOSTNAMES · BLOCKED_HOSTNAME_SUFFIXES | ['localhost'] · ['.localhost'] — odmietnuté podľa zápisu. |
isPrivate(string $address): bool | V tabuľke, alebo nesie adresu, ktorá v nej je; nečitateľná → true; zone id sa ignoruje. |
isMetadata(string $address): bool | Cloudový metadátový koncový bod alebo IPv6 adresa, ktorá ho nesie. |
isBlockedHostname(string $host): bool | localhost alebo *.localhost, v akejkoľvek veľkosti písmen, jedna koncová bodka sa vynechá. |
carriedIpv4(string $address): ?string | IPv4 adresa, ktorú nesie IPv6 adresa, v bodkovom zápise; null pre IPv4, adresu, ktorá žiadnu nenesie, alebo nečitateľný vstup. |
use RoundlyConsulting\SafeFetch\Support\PrivateNetworks;
PrivateNetworks::carriedIpv4('2002:a9fe:a9fe::'); // '169.254.169.254' — 6to4
PrivateNetworks::carriedIpv4('64:ff9b::808:808'); // '8.8.8.8' — NAT64
PrivateNetworks::isMetadata('::ffff:169.254.169.254'); // truePomocné metódy na kontrolu
Handly a overený cieľ ukazujú, čo ochrana vypočíta, bez toho, aby sa čokoľvek odoslalo:
SafeFetch::networks()->isPrivate('169.254.169.254'); // true — cloud metadata
SafeFetch::networks()->isPrivate('::ffff:10.0.0.1'); // true — judged by the IPv4 address it carries
SafeFetch::networks()->isBlockedHostname('api.localhost'); // true
SafeFetch::networks()->blockedRanges(); // the 26 ranges, IPv4 and IPv6
$url = SafeFetch::urls()->canonicalise('HTTPS://Example.COM.:443/a?b#frag');
$url->toString(); // 'https://example.com/a?b'
$url->origin(); // 'https://example.com:443'
SafeFetch::urls()->resolveRedirect('https://example.com/a/b', '../c'); // 'https://example.com/c'
$vetted = SafeFetch::vet('https://hooks.example/in');
$vetted->addresses; // every address the name resolved to, each judged
$vetted->resolveEntry(); // 'hooks.example:443:93.184.216.34,[2606:2800:220:1:248:1893:25c8:1946]'
$vetted->connectsTo('::ffff:93.184.216.34'); // true — the mapped form is the same addresscanonicalise() vyhodí DestinationRejected s dôvodom parsera (invalid_url, scheme, credentials, non_canonical_host, port); resolveRedirect() ju vyhodí (invalid_url) pre relatívny odkaz, na ktorom sa parsery nezhodnú.
Vlastný resolver
Každý preklad ide cez Contracts\HostResolver — jedinú metódu resolve(string $host): array, ktorá vráti všetky IPv4 a IPv6 adresy (IPv6 bez zátvoriek), alebo [], ak sa meno nepreloží. Hostiteľ, ktorého dostane, je už kanonický. Predvolený Support\SystemHostResolver používa gethostbynamel() (systémový resolver vrátane súboru hosts) a dns_get_record(DNS_AAAA). Čokoľvek resolver vráti, sa aj tak posúdi a pripne: privátnu adresu nepustí a odpoveď, ktorá nie je adresa, sa odmietne.
use RoundlyConsulting\SafeFetch\Contracts\HostResolver;
final class DnsRecordResolver implements HostResolver
{
public function resolve(string $host): array
{
$addresses = [];
foreach (@dns_get_record($host, DNS_A | DNS_AAAA) ?: [] as $record) {
$addresses[] = $record['ip'] ?? $record['ipv6'] ?? null;
}
return array_values(array_unique(array_filter($addresses, is_string(...))));
}
}// config/safe-fetch.php
'resolver' => App\Support\DnsRecordResolver::class,
// or in a service provider
app()->instance(HostResolver::class, new DnsRecordResolver);Trieda, ktorá nie je inštancovateľný HostResolver, vyhodí pri vytvorení InvalidConfigurationException. V testoch použite namiesto toho SafeFetch::fakeDns().
Prejavte lásku k open source
Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.
Ďalšie spôsoby podpory vrátane kryptomienOdoslaním daru súhlasíte s našimi podmienkami prijímania darov.
Chcete to zabudovať do svojho produktu?
Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.