The jwt hook flags JWT verification without an algorithm pin, the algorithm-confusion hole where a verifier trusts the token's own alg header, and verification turned off entirely. It is language-aware, reading each file by its own JWT library, and it is the deterministic engine behind the federation sub-skill, which you can run yourself in scan or check mode.
Run it
- Scan the codebase
- Check a call
node ./.lagune/hooks/jwt.mjs # scans the whole project
node ./.lagune/hooks/jwt.mjs -d src # scans a directory
node ./.lagune/hooks/jwt.mjs -f auth.ts # scans a single file
node ./.lagune/hooks/jwt.mjs -l javascript -p 'jwt.verify(token, secret)' # => unpinned
node ./.lagune/hooks/jwt.mjs -l python -p 'jwt.decode(t, k, options={"verify_signature": False})' # => unpinned
node ./.lagune/hooks/jwt.mjs -l python -p 'jwt.decode(t, k, algorithms=["HS256"])' # => safe
Every line under Unpinned JWT verification found is a verify call that sets no algorithms allowlist (where the library needs one), accepts an unsafe one (none, or an asymmetric algorithm mixed with HMAC), or disables signature checking outright, so it trusts the token's own alg. A confirmed exposure, so the hook exits non-zero. A clean run prints no unpinned JWT verification found.
How to read the verdict
| Verdict | Meaning |
|---|---|
unpinned | A verify call with no algorithm allowlist (where one is needed), an unsafe one, or with verification disabled. |
safe | A verify call that pins a sound algorithm allowlist, or whose library pins it implicitly. |
invalid | The snippet contains no recognizable verify call to judge. |
CLI options
| Option | Alias | Value | Description |
|---|---|---|---|
--pattern | -p | a code snippet | Check one call. Repeat to check several, one verdict per line. |
--lang | -l | a language | Required with -p: one of javascript, python, go, java, kotlin, php, ruby, rust, csharp. |
--dir | -d | a directory | Scope a scan to a directory. Repeats and combines with -f. |
--file | -f | a file | Scope a scan to a single file. Repeats and combines with -d. |
With no option it scans the whole project. -p needs -l and cannot be combined with -d or -f.
Supported languages
The scan reads these libraries, keyed by file extension. Because some pin the algorithm implicitly (a missing explicit list is sound), the "no allowlist ⇒ unpinned" rule applies only where the library expects an explicit one, so a correctly-configured call is never flagged.
| Language | Library | Flags no-allowlist as unpinned? | Always flags |
|---|---|---|---|
| JavaScript / TypeScript | jsonwebtoken, jose | Yes | none, asymmetric+HMAC mix |
| Python | PyJWT, python-jose | Yes (PyJWT requires it, python-jose defaults algorithms=None) | none, verify_signature: False |
| Go | golang-jwt | Yes (no WithValidMethods) | none, ParseUnverified |
| Ruby | ruby-jwt | Yes | none, verify = false |
| Java / Kotlin | jjwt, nimbus | No (key-derived) | none, an unsigned/unsecured parser |
| PHP | firebase/php-jwt | No (Key carries the alg) | none |
| Rust | jsonwebtoken | No (Validation requires one) | none, insecure_disable_signature_validation |
| C# | Microsoft.IdentityModel | No (multi-line params) | none, RequireSignedTokens = false |
The hook decides only the algorithm pin: the claim, key, and flow checks are judgment the federation sub-skill still covers.
Frequently Asked Questions
A verifier that reads the algorithm from the token header lets an attacker swap RS256 for HS256 and sign with the public key. Pinning a sound algorithm allowlist in code prevents it, and the jwt hook flags a verify call that sets none or an unsafe one.
