JWT decoded without signature verification
ID |
dart.auth.jwt_signature_verification_bypass |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
high |
Remediation Effort |
medium |
Resource |
Auth |
Language |
Dart |
Description
A JWT is decoded with an API that DOES NOT verify the signature (JwtDecoder.decode, JwtDecoder.isExpired, JwtDecoder.getExpirationDate from package:jwt_decoder; JWT.decode / JWT.tryDecode from package:dart_jsonwebtoken). Using the decoded claims for authentication or authorization lets an attacker forge tokens with arbitrary identity / role.
Rationale
Two popular Dart JWT libraries each expose both a verifying API
(JWT.verify) and a parsing-only API (JwtDecoder.decode,
JWT.decode). The parsing API splits the token on ., base64-
decodes the body, and returns the claims map — without checking
the signature, the algorithm header, the expiration (exp),
not-before (nbf), audience (aud), or issuer (iss).
Using the parsed claims to decide "is this user allowed?" or "is this token still valid?" is equivalent to trusting the URL bar for authentication: the attacker forges any payload they want, encodes it as a JWT with a fake signature, and the app accepts it. This is exactly the kind of confused-deputy bug that breaks high-profile mobile apps.
JwtDecoder.isExpired is also unsafe: it parses exp from the
body but doesn’t verify the signature, so an attacker can set
any future exp they want.
The following code illustrates a vulnerable pattern detected by this rule:
class UnverifiedJwt {
Map<String, dynamic> readClaims(String token) {
// VULNERABLE: JWT decoded without signature verification
return JwtDecoder.decode(token);
}
}
Remediation
Use JWT.verify(token, SecretKey(secret)) (or JWT.tryVerify)
from dart_jsonwebtoken and check every claim that matters
(exp, nbf, aud, iss) before trusting the payload.
Verification ALWAYS requires the key — public for RS/ES
signatures, shared secret for HS.
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
// Vulnerable: no signature verification
final claims = JwtDecoder.decode(token);
if (claims['admin'] == true) ...; // trivially forgeable
// Fixed: verify signature + claims
try {
final jwt = JWT.verify(token, SecretKey(serverSecret));
if (jwt.payload['aud'] != expectedAudience) throw 'wrong aud';
if (jwt.payload['iss'] != expectedIssuer) throw 'wrong iss';
// signature, exp, nbf, aud, iss all checked
} on JWTException catch (e) {
// reject
}
References
-
OWASP Top 10 2025 - A04 : Cryptographic Failures.
-
OWASP Top 10 2025 - A07 : Authentication Failures.
-
CWE-345 — Insufficient Verification of Data Authenticity
-
package:dart_jsonwebtoken (pub.dev) — Use
JWT.verify/JWT.tryVerify;JWT.decodeis for testing only -
package:jwt_decoder (pub.dev) — This package only decodes, never verifies — do not use for auth decisions
-
Talsec — OWASP Mobile Top 10 for Flutter M3 — Insecure authentication and authorization in Flutter