CVE-2026-23993: JWT authentication bypass in HarbourJwt via “unknown alg”
I didn’t know Harbour even existed as a language when I found this bug. The fun part is that I also didn’t need to know Harbour to spot a critical flaw — I used an LLM as a first-pass reviewer, then validated the finding by reading the small, security-critical code paths myself.
This post covers CVE-2026-23993, an authentication bypass in HarbourJwt where any unrecognized JWT algorithm value in the header causes signature verification to be bypassed.
How I found it (without knowing Harbour)
I scanned a bunch of JWT libraries listed on jwt.io and ran an internal “jwt-library-review” skill (using Claude) on each…
CVE-2026-23993: JWT authentication bypass in HarbourJwt via “unknown alg”
I didn’t know Harbour even existed as a language when I found this bug. The fun part is that I also didn’t need to know Harbour to spot a critical flaw — I used an LLM as a first-pass reviewer, then validated the finding by reading the small, security-critical code paths myself.
This post covers CVE-2026-23993, an authentication bypass in HarbourJwt where any unrecognized JWT algorithm value in the header causes signature verification to be bypassed.
How I found it (without knowing Harbour)
I scanned a bunch of JWT libraries listed on jwt.io and ran an internal “jwt-library-review” skill (using Claude) on each repository. The goal wasn’t to blindly trust the output — it was to triage: quickly flag suspicious patterns so I could focus my manual review where it matters.
#!/bin/bash
for lib in ~/jwt/*/*/; do
echo ""
echo "=== $lib ==="
if [[ -f "${lib}JWT_SECURITY_REVIEW.md" ]]; then
echo "Skipping: JWT_SECURITY_REVIEW.md already present"
continue
fi
echo "Please run the jwt-library-review skill on the code in $lib"
claude --dangerously-skip-permissions -p "Please run the jwt-library-review skill on the code in $lib"
done
echo "Done!"
One repository stood out because the signature verification logic looked “too permissive” around the algorithm selection. That’s where this issue came from.
Summary
HarbourJwt incorrectly accepts forged tokens when the JWT header contains an algorithm value not in HS256, HS384, or HS512.
- Not just the classic
"alg":"none"issue (often referenced around CVE-2015-2951). - Any unknown value works:
none,None,NONE,foo,zzz, an empty string, etc. - Impact: complete authentication bypass — forge tokens without the secret key.
Where the bug lives
The vulnerable logic is in Verify() (in src/jwt.prg). The bypass happens because signature generation returns an empty string for unknown algorithms, and verification compares strings without treating the algorithm error as fatal.
Root cause: “unknown algorithm” returns an empty signature
In HarbourJwt, signature computation is performed by GetSignature(). When the algorithm is not recognized, the method sets an error string but returns an empty signature ("").
METHOD GetSignature( cHeader, cPayload, cSecret, cAlgorithm ) CLASS JWT
LOCAL cSignature := ""
DO CASE
CASE cAlgorithm=="HS256"
cSignature := ::Base64UrlEncode( ... HB_HMAC_SHA256(...) )
CASE cAlgorithm=="HS384"
cSignature := ::Base64UrlEncode( ... HB_HMAC_SHA384(...) )
CASE cAlgorithm=="HS512"
cSignature := ::Base64UrlEncode( ... HB_HMAC_SHA512(...) )
OTHERWISE
::cError := "INVALID ALGORITHM"
// cSignature remains "" (empty string)
ENDCASE
RETU cSignature
Returning an empty signature for an unknown algorithm is already dangerous — but the real issue is what happens next.
The verification bypass
During verification, HarbourJwt computes a new signature and compares it to the token signature:
cNewSignature := ::GetSignature( aJWT[1], aJWT[2], ::cSecret, aHeader[ 'alg' ] )
IF ( cSignature != cNewSignature )
::cError := "Invalid signature"
RETU .F.
ENDIF
If an attacker supplies a token with:
algset to an unsupported value (soGetSignature()returns"")- an empty JWT signature segment (so the token signature is also
"")
Then both strings match:
cSignature(from token) =""cNewSignature(computed) =""
The comparison passes, the IF block is skipped, and verification continues as if the token was valid. The error message ("INVALID ALGORITHM") is set but not treated as a verification failure.
Exploit sketch
JWTs are three dot-separated segments: base64url(header).base64url(payload).base64url(signature). If the signature is empty, you still get a trailing dot.
// Header (example)
{"typ":"JWT","alg":"zzz"}
// Payload (example)
{"sub":"admin","name":"Mallory","iat":1700000000,"role":"admin"}
An attacker base64url-encodes header and payload, and sends:
<base64url(header)>.<base64url(payload)>.
No secret key required. No cryptography required. Just string comparison with an empty signature.
Impact
- Complete authentication bypass
- User impersonation (forge any identity /
sub) - Privilege escalation (forge any claims/roles)
- Trivial exploitation (no key material needed)
The fix
The maintainer fixed the issue in commit e1e0ee9 by making the algorithm error state part of the verification decision (and by resetting the error before verification).
@@ -218,6 +218,9 @@ METHOD Verify( cJWT ) CLASS JWT
LOCAL aJWT, aHeader, aPayload
LOCAL cSignature, cNewSignature
+ // Reset error
+ ::cError := ''
+
@@ -256,7 +259,7 @@
cNewSignature := ::GetSignature( aJWT[1], aJWT[2], ::cSecret, aHeader[ 'alg' ] )
- IF ( cSignature != cNewSignature )
+ IF ( cSignature != cNewSignature .OR. !EMPTY(::cError) )
::cError := "Invalid signature"
RETU .F.
ENDIF
In plain terms: unknown algorithm now leads to a failed verification, even if both signatures would have been empty strings.
Regression tests
The same patch also adds/extends tests to cover multiple algorithms and to prevent regressions around invalid algorithms and signatures. That’s exactly what you want after fixing a bug in a security boundary: a test that fails loudly if the bypass ever comes back.
Related: another JWT issue found using the same approach
This wasn’t the only JWT issue I found while doing automated triage + manual validation. Using the same “jwt-library-review” workflow, I also found and reported a JWT signature verification bypass in a Swift library: GHSA-88q6-jcjg-hvmw.
Takeaway: AI can triage, but you still need a manual code review foundation
Tools can help you go faster, but they don’t replace understanding. The reason this bug was easy to confirm is that the verification boundary is small — and manual review skills let you zoom in on the exact failure mode.
If you want to get better at finding issues like this (in any language — including the ones you “didn’t know existed”), check out our live training on manual security code review: https://pentesterlab.com/live-training/. The training is focused on building strong fundamentals in manual review, and that foundation is a key step if you want to reliably automate reviews later with AI agents or other tooling.
References
- HarbourJwt repository
- Fix commit (e1e0ee9)
- jwt.io libraries index
- GHSA-88q6-jcjg-hvmw advisory (jose-swift)

Written by Louis Nyffenegger
Founder and CEO @PentesterLab