Every CVESearch/Get API key

GHSA-2jx3-65f3-xr8r

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

6.9 MediumFix available

GHSA-2jx3-65f3-xr8r is a medium vulnerability (CVSS 6.9) in spomky-labs/otphp, published June 18, 2026. No source lists public exploit code. Fixed in spomky-labs/otphp 11.4.3.

What does this mean?
Severity
6.9 MediumCVSS 4.0, GitHub Advisories
Exploit chance
UnscoredNo EPSS score yet
Exploitation
None knownNot on CISA’s or ENISA’s lists

Description

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.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
None
Integrity
Low
Availability
Low

Affected packages

Affected packages
PackageAffectedFixed
spomky-labs/otphp
Packagist
before 11.4.3
11.4.3
spomky-labs/otphp
Packagist
before 11.4.3, plus listed versions
1.0.1-stable11.0.311.1.011.1.111.2.0
Show 45 moreShow less
11.2.111.2.211.3.011.4.011.4.111.4.25.0.0v1.0.0-stablev10.0.0v10.0.1v10.0.2v10.0.3v11.0.0v11.0.1v11.0.2v2.0.0-stablev2.0.1-stablev2.0.2-stablev3.0.0-stablev3.0.1-stablev3.1.0v3.1.1v4.0.0v4.0.1v4.0.2v4.0.3v4.0.4v5.0.1v6.0.0v6.0.1v6.0.2v6.0.3v6.0.4v7.0.0v7.0.1v7.0.2v7.0.3v7.0.4v8.0.0v8.1.0v8.2.0v8.3.0v8.3.1v8.3.2v8.3.3
11.4.3

What each source reports

GitHub Advisories Score used6.9

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

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.

Record
GHSA-2jx3-65f3-xr8r
CVSS 4.0
6.9 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N
Weakness
CWE-915
Updated
2026-11-02
OSV6.9

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

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.

Record
GHSA-2jx3-65f3-xr8r
CVSS 4.0
6.9 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N
Weakness
CWE-915
Updated
2026-11-02

History

History
RecordedChangedSources
first indexedGitHub Advisories, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

References

This page doesn't list everything

Left out here: 10 versions. The JSON record has them.

MoveEnter Open