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
}

Configuration

This detector does not need any configuration.

References

© 2026 Xygeni. All rights reserved