Skip to content

Bug: PwnedValidator is broken: base_uri option is never read by CodeIgniter's CURLRequest #1371

Description

@adnduweb

PHP Version

8.4

CodeIgniter4 Version

v4.7.4

Shield Version

v1.4.0

Which operating systems have you tested for this bug?

macOS

Which server did you use?

apache

Database

MYSQL 5

Did you customize Shield?

no

What happened?

PwnedValidator never reaches the HaveIBeenPwned API. Every password check
fails with a DNS error, because the request is sent to a host literally named
range instead of api.pwnedpasswords.com.

The cause is an option-key mismatch: PwnedValidator passes the Guzzle
option name base_uri, but CodeIgniter's CURLRequest only ever reads
baseURI.

Since PwnedValidator is commented out in the default Config\Auth, this
stays invisible until someone enables it — at which point every password
change and every registration fails
, since Shield converts the failure into
a thrown AuthenticationException.

Steps to reproduce

  1. Enable the validator in app/Config/Auth.php:

    public array $passwordValidators = [
        CompositionValidator::class,
        NothingPersonalValidator::class,
        DictionaryValidator::class,
        PwnedValidator::class,   // <- enabled
    ];
  2. Attempt any password validation (registration, or a rule using
    strong_password).

  3. Observed result:

    CodeIgniter\Shield\Exceptions\AuthenticationException
    cURL error 6: Could not resolve host: range
    Request URL: http://range/1B485
    

    Note the URL: http://range/1B485. The base URI was never applied, so the
    relative path range/<prefix> was treated as a hostname.

Steps to Reproduce

In short: the option key Shield sends is not the one CodeIgniter reads.
Shield passes the Guzzle-style base_uri; CodeIgniter's CURLRequest only
ever looks for baseURI. The base URI is therefore never applied, and the
relative path is resolved against an empty base.

Line numbers below are as of CodeIgniter v4.7.4 and Shield v1.4.0; the
logic is what matters, and it is unchanged across recent versions — develop
still passes base_uri at the time of writing.

PwnedValidator::check() — src/Authentication/Passwords/PwnedValidator.php

$client = Services::curlrequest([
    'base_uri' => 'https://api.pwnedpasswords.com/',   // Guzzle-style key
]);

$response = $client->get('range/' . $rangeHash, [...]);

CodeIgniter never reads that key. Both places that could apply it look for
baseURI:

system/Config/Services.phpcurlrequest() (as of CI 4.7.4)

return new CURLRequest(
    $config,
    new URI($options['baseURI'] ?? null),   // 'base_uri' -> null
    $response,
    $options,
);

system/HTTP/CURLRequest.phpparseOptions() (line 311 as of CI 4.7.4)

if (array_key_exists('baseURI', $options)) {      // 'base_uri' -> false
    $this->baseURI = $this->baseURI->setURI($options['baseURI']);
    unset($options['baseURI']);
}

So $this->baseURI stays empty, and CURLRequest::prepareURL() (line 352 as
of CI 4.7.4) does:

$uri = $this->baseURI->resolveRelativeURI($url);   // resolves against nothing

which yields range/1B485. cURL then interprets the first segment as a host.

Expected Output

One-line change in PwnedValidator::check():

  $client = Services::curlrequest([
-     'base_uri' => 'https://api.pwnedpasswords.com/',
+     'baseURI' => 'https://api.pwnedpasswords.com/',
  ]);

Two related points worth considering, though they are separate from the bug:

  1. Services::curlrequest() returns a shared instance by default. If any
    other code created the shared curlrequest service earlier in the request,
    the options passed here are silently ignored and the base URI is whatever
    that first caller set. Passing getShared: false — or using the absolute
    URL in get() — would make the validator independent of call order.

  2. An unreachable API currently blocks all password changes. A network
    failure or an HIBP outage raises AuthenticationException, so users cannot
    change their password at all. Treating an unreachable API as "not verified"
    (log a warning, let the password through, the other validators still apply)
    would avoid making a third-party outage a site-wide lockout. Happy to open a
    separate issue for that if you prefer to keep this one focused on the key
    name.

Anything else?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions