CWE-347: Improper Verification of Cryptographic Signature
Overview
Improper signature verification occurs when an application accepts unsigned data, skips the signature check, accepts a weak signature algorithm, or implements the check incorrectly. Each of those lets an attacker alter signed data and have it accepted as genuine, which is enough to defeat any authentication built on the signature.
Where the weakness sits has shifted, and triage is easier if you know that. The bypasses this CWE is famous for - alg: none, and re-signing an RS256 token as HS256 with the server's public key as the HMAC secret - are refused out of the box by current versions of the mainstream JWT libraries, because they derive the acceptable algorithms from the type of key they were given. Confirm that on the version you have rather than assuming it, and note that the guards are not all the same strength: PyJWT's, for instance, recognises a key by its text shape, so a PEM public key is refused as an HMAC secret and the same key in DER form is not. What remains is subtler and entirely live:
- The application, not the library, converts the key. A key resolver that returns a genuine symmetric key because the token's header asked for one performs the confusion itself, and no library check can see it.
- Cross-algorithm acceptance inside one family. Where an algorithm is not pinned, a token minted as HS512 verifies against a service that only ever issues HS256, and RS512 or PS256 against an RS256 service. The signature is real; the token is not one your issuer would have produced.
- Key provenance taken from the token.
kidused as an index into a keystore you populated is safe.jku,x5u,jwkandx5cname where the key comes from, so honouring one lets the sender supply the key their own token verifies against. - The verified data and the used data being different copies. A signature check that proves an XML element or a request body was signed, followed by business logic that reads a different element or a re-serialized copy.
An old page, an old scanner rule or an old memory will point at the first pair. Check which of these your code actually has before writing the fix.
Relationship to Other CWEs
A finding filed against CWE-345 (Insufficient Verification of Data Authenticity) that turns out to be about a signature check belongs here instead - MITRE marks that parent DISCOURAGED for mapping. This is the right level whenever a signature mechanism exists and the verification of it is what went wrong.
The pages around it differ by what was being verified:
- CWE-347 (this page) - a signature mechanism is present but the check is wrong: skipped, an unchecked return value, the algorithm taken from the message, or the verified bytes not the bytes later used
- CWE-354 (Improper Validation of Integrity Check Value) - the same failures where the protection is a checksum, hash or MAC rather than a signature, including the case where the check runs correctly but the algorithm cannot resist deliberate tampering
- CWE-494 (Download of Code Without Integrity Check) - what goes unverified is code about to be executed. The publisher-signature check on an update or a package is this page's weakness; that page covers what it costs to get it wrong, which is remote code execution
- CWE-642 (External Control of Critical State Data) - the prior question behind a signed cookie or token: whether security-critical state should sit with the client at all. Signing it and verifying the signature properly is the fix this page describes; that page is where to look when holding the state client-side is itself the defect
OWASP Classification
A04:2025 - Cryptographic Failures
Risk
Critical: Flawed signature verification turns every guarantee the signature was there to provide into an assumption: forged JWTs that authenticate as whoever the attacker names, tampered API requests processed as authentic, software updates you never signed. Where the verifier can be pointed at a key the attacker controls, the impact is unbounded - every claim in the token, including roles and identity, becomes theirs to set.
Remediation Steps
Core Principle: Verify cryptographic signatures correctly (algorithms, cert chains, canonicalization); reject on any verification failure.
Locate the improper signature verification
- Find the file, line and code pattern the finding points at
- Name the actual defect: missing verification, accepting unsigned data, an unpinned algorithm, a key chosen by the token, or a weak algorithm
- Determine what signed data is affected: JWTs, API signatures, software updates, certificates, documents
- Trace the verification flow: where signatures are checked (or not checked) before trusting data
- Establish what the installed library already refuses before writing anything. Mint the forged token the finding describes and feed it to the current code. If it is already rejected, the finding is about a different weakness than the tool's description says, and fixing the described one changes nothing. This costs a few minutes and is the difference between a fix and a no-op
Implement robust signature verification (Primary Defense)
- Verify before trusting: no code path acts on signed data before the signature has been checked, and data that arrives unsigned stays untrusted
- Use the language's constant-time comparison for raw signatures and HMACs: every runtime ships one, and none of them is
==. Check what it does with a length mismatch as well as a value mismatch - some return false, some raise - Act on the data that was verified, not a second copy of it: verify the raw request bytes and parse those same bytes; confirm a validated XML
<Reference>covers the element you go on to read - Fail on any verification failure: do not log the failure and carry on - the request ends there
- Never skip verification in production: No environment-gated bypass. If one exists, ask which branch an unset variable selects - "not production" is the usual answer and the dangerous one
Pin the signature algorithm
- Allowlist the exact algorithm, not the family: an allowlist of one is the goal. A family check (
RSA,HMAC) still admits every digest size in that family, which is the gap that survives on most current libraries - Reject "none": current libraries do this without being asked, but say so explicitly - it costs nothing and covers an older dependency
- Don't let the token switch families: a resolver that returns different key types depending on the header is performing the confusion itself, and no library check will stop it
- Use strong signature schemes: RSA-3072 for new keys (2048 is 112-bit and NIST-accepted only through 2030), 256+ bit ECDSA, or Ed25519
Proper JWT verification
- Fix the key before reading the token: the verification key comes from configuration, a keystore, or a JWKS cache the application fetched. Never from the token
- Use
kidas an index, never as a source:kidselecting an entry in a table you populated is fine.jku,x5u,jwkandx5cname where key material comes from, so honouring one lets the sender choose the key - andjku/x5ualso make your verifier fetch an attacker-supplied URL (CWE-918) - Pin the algorithm explicitly: in the parser where the library supports it, and as an assertion on the parsed header where it does not
- Check exp, iat, nbf claims - and require the ones you rely on: most libraries validate a claim when present and accept a token that omits it entirely
- Validate issuer (iss) and audience (aud): the token must come from a trusted issuer and be intended for your application
- Use an established library, not your own implementation: jsonwebtoken (Node.js), PyJWT (Python), Nimbus JOSE+JWT or jjwt (Java),
firebase/php-jwt(PHP), golang-jwt (Go),Microsoft.IdentityModel.JsonWebTokens(.NET)
Certificate/key validation
- Verify the whole chain, from the leaf to a trusted root CA
- Check the certificate is currently valid (notBefore <= now <= notAfter)
- Check the certificate CN/SAN matches the hostname being connected to
- Verify with the signer's own public key
Test the signature verification fix
- Start with the accept. Confirm a legitimate token and a legitimately signed payload are still processed. A verifier that refuses everything passes every rejection test below
- Send a JWT with no signature and verify it is rejected
- Tamper with the signature or the signed data and verify rejection
- Test cross-algorithm acceptance: re-sign a valid token as the next digest size up in the same family (RS256 to RS512, HS256 to HS512) using the real key, and verify rejection. This is usually the only assertion in the list whose result changes when the fix lands
- Test alg=none and cross-family confusion (HS256 signed with the RSA public key), and note that on a current library both are refused before your fix - keep them as regression tests, not as evidence the fix works
- Test the malformed input, not only the wrong one: a signature header of odd length, containing non-hex characters, or containing a byte above 0x7F. Each must produce the same rejection as a wrong signature. A 500 here means a decoder or comparison primitive is throwing on exactly the input an attacker sends
- Re-scan with the security scanner to confirm the finding is resolved
Common Vulnerable Patterns
- Not verifying the signature at all - decoding a token and reading its claims, or checking a hash where a signature was required
- Letting the token's header choose the verification key, whether through a resolver that branches on
algor one that fetches from ajku/x5uURL - Verifying without pinning an algorithm, so any algorithm the configured key supports is accepted
- Verifying one copy of the data and acting on another - a re-serialized request body, or an XML element other than the one the signature referenced
- Comparing signatures or HMACs with
==instead of a constant-time function - Accepting a JWT with
alg: none- still worth checking on an unmaintained dependency, refused by default on current ones - Custom signature verification code
Language-Specific Guidance
How much of the algorithm pinning the library does for you differs by ecosystem, which is why the fix is not the same sentence everywhere:
- C# -
ValidAlgorithmspinning, whatValidateIssuerSigningKeyreally gates,SignedXml.CheckSignature'sverifySignatureOnlyparameter and XML wrapping - Go - golang-jwt keyfunc method assertions (family, not algorithm) plus
jwt.WithValidMethods,hmac.Equalfor webhook HMACs - Java -
Signature.verify()return-value checks, NimbusJWSKeySelectorpinning against jjwtverifyWith()which does not, XML Signature Wrapping - JavaScript -
jsonwebtokenalgorithms allowlist,decode()misuse,crypto.timingSafeEqualfor webhooks - PHP -
firebase/php-jwtKeybinding, which pins the exact algorithm;hash_equalsfor webhook HMACs - Python - PyJWT algorithms list,
hmac.compare_digestand its non-ASCIITypeError,cryptographylibrary verification
Additional Resources
- CWE-347: Improper Verification of Cryptographic Signature
- NIST SP 800-57 Part 1 Rev 5: Recommendation for Key Management - Table 4 is the source for the 2030 date: 112-bit strength is Acceptable for applying protection (generating keys, encrypting, signing) through 2030 and Disallowed from 2031, while processing already-protected data stays permitted as legacy use
- OWASP Cheat Sheet Series
- OWASP Top 10 2025
- OWASP Top 10 2025 A04: Cryptographic Failures