← All advisories
CVE-2026-55599Medium · CVSS 5.8· CWE-918

SSRF in phpseclib X.509 Validation via Attacker-Controlled Authority Information Access URL

Vendor
phpseclib
Product
phpseclib
Status
Published · Jun 17 2026
Researchers
eo420
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N
Published
Jun 17 2026

Summary

When an application validates an untrusted X.509 certificate with phpseclib, validateSignature() reads a URL out of certificate's Authority Information Access (AIA) extension and connects to it. URL fetching is enabled by default, and an attacker who supplies certificate controls host, port, and path of that connection, which is made through fsockopen() with no destination check. This is a server-side request forgery driven by an insecure default: untrusted certificate content is turned into an outbound request to a target phpseclib should never reach.

An unauthenticated attacker who submits a certificate can make a validating server open connections to internal hosts and ports, for example loopback 127.0.0.1, cloud metadata address 169.254.169.254, and internal-only services that are not exposed outside. Fetch is blind, so it is a request-forgery and internal-reconnaissance primitive rather than a direct read of internal responses. It reproduces on current released LTS 3.0.53 and on 4.0 development line, and same default-on code is present in all 1.0.x, 2.0.x, and 3.0.x releases. Affected applications include any that validate a presented or uploaded certificate, such as client-certificate checks, S/MIME or CMS signer verification, and document-signing validation. No authentication and no user interaction are needed.

Technical Details: Root Cause

AIA chasing reads a URL from untrusted certificate

When no already-trusted certificate authority is issuer of certificate under validation, validateSignatureCountable() continues to AIA fetching. Default for validateSignature() is caonly = true:

// phpseclib/File/X509.php:1316-1327 (4.0 development line, commit 74ada1a6)
if (!isset($signingCert)) {
    if ($caonly) {
        return $this->testForIntermediate(true, $count) && $this->validateSignature(true);
    } else {
        try {
            $this->testForSelfSigned();
            $signingCert = $this;
        } catch (BadMethodCallException) {
            return $this->testForIntermediate(true, $count) && $this->validateSignature(true);
        }
    }
}

testForIntermediate() takes URL straight out of certificate's AIA caIssuers field, which an attacker fully controls, and passes it to fetchURL():

// phpseclib/File/X509.php:1357-1391
$opts = $this->getExtension('id-pe-authorityInfoAccess');
...
foreach ($opts['extnValue'] as $opt) {
    if ($opt['accessMethod'] == 'id-ad-caIssuers') {
        if (isset($opt['accessLocation']['uniformResourceIdentifier'])) {
            $url = (string) $opt['accessLocation']['uniformResourceIdentifier']; // attacker controlled
            break;
        }
    }
}
...
$cert = static::fetchURL($url); // server-side request forgery

No destination check in fetchURL, and on by default

fetchURL() connects to attacker host and port. There is no block on loopback, link-local, private, or metadata ranges, and no port restriction:

// phpseclib/File/X509.php:1456-1476
private static function fetchURL(string $url): ?string
{
    if (self::$disable_url_fetch) {  // default false, so fetching happens
        return null;
    }
    $parts = parse_url($url);
    switch ($parts['scheme']) {
        case 'http':
            $fsock = @fsockopen($parts['host'], $parts['port'] ?? 80); // attacker host and port
            ...
            fputs($fsock, "GET $path HTTP/1.0\r\n");
            fputs($fsock, "Host: $parts[host]\r\n\r\n");

Fetching defaults to on:

// phpseclib/File/X509.php:110
private static bool $disable_url_fetch = false;

Same default-enabled logic ships in released 3.0.x. In 3.0.53 it sits at $disable_url_fetch = false on line 255 and fsockopen($parts['host'], ...) on line 1136 of phpseclib/File/X509.php, which is why this reproduces on production code, not only on 4.0.

Why it is a vulnerability and not just a feature

AIA chasing is a legitimate capability from RFC 4325, so fetching by itself is not wrong. Vulnerability is combination that defines an SSRF:

  • URL comes from untrusted input, namely certificate an application is trying to validate.
  • Fetching is enabled by default, so a caller who simply runs validateSignature() gets outbound requests with no opt-in.
  • No destination is restricted, so loopback, private ranges, link-local metadata, and arbitrary ports are reachable.

Reachability is not narrow. Fetch triggers whenever certificate's issuer is not already trusted, which an attacker arranges by choosing any unknown issuer name, so having certificate authorities loaded does not protect a target. Because response is used only if it parses as a certificate and is otherwise discarded, this path is blind, which limits confidentiality impact but not request-forgery and reconnaissance capability.

Fix

Gate every fetch through an opt-in destination policy. A practical shape is a callback that receives host, resolved IP, port, and scheme, rejects private and reserved ranges, and lets phpseclib connect to that validated IP so a name cannot re-resolve to something internal afterward:

X509::setURLFetchCallback(function (string $host, string $ip, int $port, string $scheme): bool {
    if ($scheme !== 'http' && $scheme !== 'https') {
        return false;
    }
    return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
});

On 4.0, treating "no callback" as deny makes behaviour secure by default. On 3.0, where flipping default would be a compatibility break, exposing this callback (and keeping disableURLFetch() as a hard override) closes issue without breaking existing callers.