{"record":{"id":"6a1689cea3932350","repo":"ellite/Wallos","slug":"label-must-not-contain-a-colon","errorCode":null,"errorMessage":"Label must not contain a colon.","messagePattern":"Label must not contain a colon\\.","errorType":"exception","errorClass":"InvalidArgumentException","httpStatus":null,"severity":"error","filePath":"libs/OTPHP/OTP.php","lineNumber":103,"sourceCode":"            if (isset($options[$key]) && $default === $options[$key]) {\n                unset($options[$key]);\n            }\n        }\n\n        ksort($options);\n    }\n\n    /**\n     * @param non-empty-string $type\n     * @param array<non-empty-string, mixed> $options\n     *\n     * @return non-empty-string\n     */\n    protected function generateURI(string $type, array $options): string\n    {\n        $label = $this->getLabel();\n        is_string($label) || throw new InvalidArgumentException('The label is not set.');\n        $this->hasColon($label) === false || throw new InvalidArgumentException('Label must not contain a colon.');\n        $options = [...$options, ...$this->getParameters()];\n        $this->filterOptions($options);\n        $params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&'));\n\n        return sprintf(\n            'otpauth://%s/%s?%s',\n            $type,\n            rawurlencode(($this->getIssuer() !== null ? $this->getIssuer() . ':' : '') . $label),\n            $params\n        );\n    }\n\n    /**\n     * @param non-empty-string $safe\n     * @param non-empty-string $user\n     */\n    protected function compareOTP(string $safe, string $user): bool\n    {","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/ellite/Wallos/blob/52820e87ca5a6e105fdbb7f1c0c681bc0cfee2fd/libs/OTPHP/OTP.php#L85-L121","documentation":"OTP::generateURI rejects labels containing a colon via hasColon(), throwing InvalidArgumentException. In the otpauth scheme the colon separates issuer from account in labels, and this library wants the issuer supplied separately (setIssuer) rather than embedded in the label.","triggerScenarios":"setLabel('My Company: alice@example.com') followed by getProvisioningUri(); labels copied from other tools that embed 'Issuer:account' in one string.","commonSituations":"Migrating from libraries that expect 'Issuer:user' labels; concatenating issuer and username manually; copy-pasting full labels from decoded QR payloads.","solutions":["Remove the colon from the label: use setLabel('alice@example.com').","Set the issuer separately: $otp->setIssuer('My Company') — the library renders 'Issuer:account' correctly itself.","Strip or replace colons: str_replace(':', ' ', $label) before setLabel().","Validate labels with strpos($label, ':') === false before setting."],"exampleFix":"// before\n$otp->setLabel('Acme: alice@example.com');\n// after\n$otp->setIssuer('Acme');\n$otp->setLabel('alice@example.com');","handlingStrategy":"validation","validationCode":"if (str_contains($label, ':')) {\n    [$issuer, $account] = explode(':', $label, 2);\n    $otp->setIssuer(trim($issuer));\n    $label = trim($account);\n}\n$otp->setLabel($label);","typeGuard":"function isCleanLabel(string $label): bool {\n    return $label !== '' && !str_contains($label, ':');\n}","tryCatchPattern":"try {\n    $uri = $otp->getProvisioningUri();\n} catch (InvalidArgumentException $e) {\n    log_error('Bad OTP label', ['label' => $otp->getLabel()]);\n    throw new InvalidOtpConfigurationException($e);\n}","preventionTips":["Never embed 'Issuer:account' in the label; use setIssuer() instead","Sanitize user-chosen display names before using them as labels","Normalize labels on import from other authenticator ecosystems","Validate label format in unit tests for enrollment flows"],"tags":["php","otp","label","validation"],"backgroundTag":"invalid-argument-format","analyzedSha":"52820e87ca5a6e105fdbb7f1c0c681bc0cfee2fd","analyzedAt":"2026-09-13T14:09:30.873Z","contentChangedAt":"2026-09-13T14:09:30.873Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}