August 16, 2025
While performing AppSec reviews on our internal products, I repeatedly encountered JWT implementations with subtle but critical misconfigurations — things like alg: none
acceptance, weak or fallback secrets, algorithm confusion between symmetric and asymmetric keys, and missing claim validation. These mistakes can enable complete authentication bypass, long-lived token replay, or privilege escalation. This post walks through what I found, how I validated the issues, and practical fixes you can apply today.
JSON Web Tokens (JWTs) are ubiquitous for stateless authentication and API authorization. Their portability and simplicity make them attractive, but those same qualities mean that small mistakes in generation or verification become powerful attack vectors.
During routine AppSec reviews of our internal services, I traced token issuance and verification logic, inspected deployment manifests, and tested token handling end-to-end. The intent of this write-up is to share concrete, realistic patterns I saw in production-like environments, how they translate into attack paths, and how teams can avoid them.
alg: none
acceptanceSome verification flows did not explicitly reject tokens that declared "alg": "none"
. If a server accepts such tokens, an attacker can strip the signature and submit a tampered payload that the backend treats as authentic.
This is not theoretical: misconfigured verification libraries or permissive verification flags are usually the root cause.
Result: trivial authentication bypass and user impersonation.
I saw secrets present in code, example .env
files, and configuration fallbacks such as "secret"
, "admin123"
, or "changeme"
. Worse, some services relied on an environment fallback instead of failing fast when the secret was not provided.
How I found them:
.env.example
filesos.getenv
-style fallbacksSome services intended to use RS256 but accidentally passed the public key into HMAC verification or allowed algorithm downgrades. If the server is not strict about the algorithm, an attacker can sign a token using public key material and have it accepted. Root causes include mixing verification code across environments and reusing configuration variables incorrectly.
Frontends parsing JWTs to decide UI state or show “admin” options without verifying the token server-side can leak sensitive information or present false privileges. While proper APIs often still enforce backend checks, client-side trust confuses threat boundaries and creates potential for information disclosure.
Practical steps I used during internal reviews:
alg: none
and algorithm downgrades (HS<->RS) against verification endpoints.All testing was done in authorized contexts against internal or staging environments per our AppSec policy ;)
While reviewing an internal authentication microservice as part of a scheduled AppSec review, I traced how tokens were issued and verified. In the token utility module I found:
JWT_SECRET = os.getenv("JWT_SECRET", "s3cr3t")
Using environment variables with a local fallback is common for development, but I validated the deployment pipeline and Helm chart and discovered that the production manifest did not set JWT_SECRET
. The service was therefore using the fallback "s3cr3t"
in production as the HMAC signing key.
To confirm the impact, I crafted a token payload with an admin role and reasonable timestamps, signed it locally using "s3cr3t"
, and submitted it to an admin-level API endpoint. The backend accepted the token and granted elevated privileges.
This issue was caused by multiple operational gaps:
Takeaway: Secrets must never have insecure defaults. Infrastructure-as-code and CI should enforce presence and strength of critical secrets, and services should refuse to start if essential secrets are missing.
Mitigations I applied with the team:
JWT_SECRET
is missing or matches common weak patterns.JWTs are powerful tools for stateless authentication, but small implementation mistakes make them fragile. During AppSec reviews of our internal products I saw the same patterns repeat: defaults left in code, missing infrastructure checks, algorithm confusion, and lax claim validation.
Security is not a checkbox — it’s an operational discipline. Require strong configuration gating, validate everything on the backend, and bake JWT verification checks into both code and deployment pipelines. Doing so reduces the chance that a trivial oversight becomes a critical incident.
🧵 Thanks for reading! If you’re working with JWTs, take a moment to review your configs and make sure no insecure defaults are slipping through. And if you’d like to swap ideas or collaborate on building safer authentication systems, I’d love to connect.