spomky-labs/otphp: Mass-assignment in Factory::loadFromProvisioningUri lets a hostile provisioning URI corrupt OTP state or leak an uncaught TypeError
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¶meters[foo]=baroverwrites the whole internal$parametersarray thatcreateFromSecret()primed (period,algorithm,digits,epoch). The resulting object is silently unusable:getProvisioningUri(),getDigits(),at(),verify()then throwParameterNotFoundException. - Uncaught TypeError escaping the documented exception type. A URI such as
otpauth://totp/Alice?secret=JBSWY3DPEHPK3PXP&issuer_included_as_parameter=notaboolassigns a string to a typedboolproperty and raises aTypeError. Thetry/catchinloadFromProvisioningUri()only wrapsUrl::fromString();createOTP()andpopulateOTP()run outside it, so theTypeError(andErroron the readonlyclock) escapes past the documentedInvalidProvisioningUriException, breaking callers that catch only the documented type. - Label/issuer validation bypass.
parameters[label]=hijackedstores a label into the parameters array without running thelabelvalidation callback (keyed onlabel, notparameters).getLabel()andgetParameter('label')then disagree — a confused-deputy risk.
Affected component
src/OTP.php:187-201—setParameter()dynamic property writesrc/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¶meters[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.
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"
]
}