GIVE YOUR AGENT THE EVIDENCE

Set up in your agent.

One prompt to get started. Paste it into your coding agent to connect cve.new through MCP and bring vulnerability context into your workflow.

Your agent needs remote MCP support and access to https://cve.new/mcp.

Read the setup guide

GIVE YOUR AGENT THE EVIDENCE

Investigate in your agent.

Turn this advisory into a focused investigation. Ask your agent to check your project’s versions, explain the evidence, and suggest a compatible fix.

Your agent needs remote MCP support and access to https://cve.new/mcp.

Read the setup guide

GHSA-2jx3-65f3-xr8rmedium6.9

spomky-labs/otphp: Mass-assignment in Factory::loadFromProvisioningUri lets a hostile provisioning URI corrupt OTP state or leak an uncaught TypeError

No CVE assignedCheck your stack

The advisory, in context

Summary

OTPHP\Factory::loadFromProvisioningUri() parses an attacker-supplied otpauth:// URI and forwards every query key to OTP::setParameter($key, $value). setParameter() resolves the name with property_exists($this, $parameter) and performs a dynamic write $this->{$parameter} = $value (src/OTP.php:196-197). Because the query keys are entirely controlled by whoever produced the URI, a URI can target the internal properties of the OTP object that are not meant to be set from a URI: parameters, issuer, label, issuer_included_as_parameter, and (on TOTP) the readonly clock. This is an instance of object property mass-assignment (CWE-915).

Impact

The Factory is documented as the entry point for third-party provisioning URIs (e.g. QR codes from Microsoft 365 / Google Authenticator). An application that loads such a URI is exposed to:

  • State corruption. A URI such as otpauth://totp/Alice?secret=JBSWY3DPEHPK3PXP&parameters[foo]=bar overwrites the whole internal $parameters array that createFromSecret() primed (period, algorithm, digits, epoch). The resulting object is silently unusable: getProvisioningUri(), getDigits(), at(), verify() then throw ParameterNotFoundException.
  • Uncaught TypeError escaping the documented exception type. A URI such as otpauth://totp/Alice?secret=JBSWY3DPEHPK3PXP&issuer_included_as_parameter=notabool assigns a string to a typed bool property and raises a TypeError. The try/catch in loadFromProvisioningUri() only wraps Url::fromString(); createOTP() and populateOTP() run outside it, so the TypeError (and Error on the readonly clock) escapes past the documented InvalidProvisioningUriException, breaking callers that catch only the documented type.
  • Label/issuer validation bypass. parameters[label]=hijacked stores a label into the parameters array without running the label validation callback (keyed on label, not parameters). getLabel() and getParameter('label') then disagree — a confused-deputy risk.

Affected component

  • src/OTP.php:187-201 — setParameter() dynamic property write
  • src/Factory.php:50-55 — populateParameters() forwarding all query keys

Proof of concept

use OTPHP\Factory;

// State corruption
$otp = Factory::loadFromProvisioningUri(
    'otpauth://totp/Alice?secret=JBSWY3DPEHPK3PXP&parameters[foo]=bar',
    $clock
);
$otp->getProvisioningUri(); // ParameterNotFoundException: Parameter "period" does not exist

// Uncaught TypeError
Factory::loadFromProvisioningUri(
    'otpauth://totp/Alice?secret=JBSWY3DPEHPK3PXP&issuer_included_as_parameter=notabool',
    $clock
); // TypeError escapes InvalidProvisioningUriException

Remediation

Restrict the keys accepted from a provisioning URI to a known allow-list of public OTP parameters, and never let a URI key resolve to an internal object property via property_exists. Route all URI-sourced values through the validated parameter map only.

Affected packages

Start with the recorded fix for your release branch. Check your installed version and test compatibility before upgrading. Fixing this advisory does not rule out other vulnerabilities in the release.

Ecosystem versions

Earlier releases before 11.4.3

Fixed in 11.4.3

60 explicitly affected versions

1.0.1-stable, 11.0.3, 11.1.0, 11.1.1, 11.2.0, 11.2.1, 11.2.2, 11.3.0, 11.4.0, 11.4.1, 11.4.2, 5.0.0, v1.0.0-stable, v10.0.0, v10.0.1, v10.0.2, v10.0.3, v11.0.0, v11.0.1, v11.0.2, v2.0.0-stable, v2.0.1-stable, v2.0.2-stable, v3.0.0-stable, v3.0.1-stable, v3.1.0, v3.1.1, v4.0.0, v4.0.1, v4.0.2, v4.0.3, v4.0.4, v5.0.1, v6.0.0, v6.0.1, v6.0.2, v6.0.3, v6.0.4, v7.0.0, v7.0.1, v7.0.2, v7.0.3, v7.0.4, v8.0.0, v8.1.0, v8.2.0, v8.3.0, v8.3.1, v8.3.2, v8.3.3, v9.0.0, v9.0.0-alpha1, v9.0.1, v9.0.2, v9.0.3, v9.1.0, v9.1.1, v9.1.2, v9.1.3, v9.1.4

View affected-version evidence
{
  "ranges": [
    {
      "events": [
        {
          "introduced": "0"
        },
        {
          "fixed": "11.4.3"
        }
      ],
      "type": "ECOSYSTEM"
    }
  ],
  "versions": [
    "1.0.1-stable",
    "11.0.3",
    "11.1.0",
    "11.1.1",
    "11.2.0",
    "11.2.1",
    "11.2.2",
    "11.3.0",
    "11.4.0",
    "11.4.1",
    "11.4.2",
    "5.0.0",
    "v1.0.0-stable",
    "v10.0.0",
    "v10.0.1",
    "v10.0.2",
    "v10.0.3",
    "v11.0.0",
    "v11.0.1",
    "v11.0.2",
    "v2.0.0-stable",
    "v2.0.1-stable",
    "v2.0.2-stable",
    "v3.0.0-stable",
    "v3.0.1-stable",
    "v3.1.0",
    "v3.1.1",
    "v4.0.0",
    "v4.0.1",
    "v4.0.2",
    "v4.0.3",
    "v4.0.4",
    "v5.0.1",
    "v6.0.0",
    "v6.0.1",
    "v6.0.2",
    "v6.0.3",
    "v6.0.4",
    "v7.0.0",
    "v7.0.1",
    "v7.0.2",
    "v7.0.3",
    "v7.0.4",
    "v8.0.0",
    "v8.1.0",
    "v8.2.0",
    "v8.3.0",
    "v8.3.1",
    "v8.3.2",
    "v8.3.3",
    "v9.0.0",
    "v9.0.0-alpha1",
    "v9.0.1",
    "v9.0.2",
    "v9.0.3",
    "v9.1.0",
    "v9.1.1",
    "v9.1.2",
    "v9.1.3",
    "v9.1.4"
  ]
}

References