Skip to content

fix(auth): prevent false positives in JWT-none and expired-JWT tests on public endpoints#67

Merged
GovindarajanL merged 4 commits into
OWASP:mainfrom
GovindarajanL:fix/jwt-none-false-positive
May 19, 2026
Merged

fix(auth): prevent false positives in JWT-none and expired-JWT tests on public endpoints#67
GovindarajanL merged 4 commits into
OWASP:mainfrom
GovindarajanL:fix/jwt-none-false-positive

Conversation

@GovindarajanL

Copy link
Copy Markdown
Collaborator

Problem

The JWT none algorithm test (ASTF-API2-2023) was producing massive false positives — 548 CRITICAL findings — when scanning Rocket.Chat's public open.rocket.chat instance.

Root Cause

Both testJwtNoneAlgorithm and testExpiredJwt only checked endpoint.isRequiresAuthentication() (which defaults to true for all inline YAML endpoints) but never verified at runtime whether the endpoint actually demands authentication.

Public endpoints like /api/info and /api/v1/settings.public return HTTP 200 to all requests — with or without a token. The test sent a JWT-none token, saw HTTP 200, and flagged CRITICAL — but the 200 had nothing to do with the token.

Proof: Manual curl with JWT-none token against auth-required endpoints (e.g. /api/v1/me) correctly returns 401. The scanner's findings were wrong.

Fix

Added a baseline probe (no Authorization header) before each JWT attack probe in both methods:

Baseline (no auth)  →  2xx  →  endpoint is public  →  SKIP (would be false positive)
Baseline (no auth)  →  4xx  →  endpoint requires auth  →  proceed with JWT-none probe
JWT-none probe      →  2xx  →  CRITICAL finding raised (genuine bypass confirmed)
JWT-none probe      →  4xx  →  no finding (correctly rejected)

The finding evidence now also includes the baseline HTTP status:

"Server returned HTTP 200 when presented with a JWT using 'none' algorithm (baseline without auth: HTTP 401)"

Tests

  • BrokenAuthenticationTestcaseTest: 3 new tests

    • testJwtNoneAlgorithmNoFalsePositiveOnPublicEndpoint — public endpoint must NOT trigger JWT-none finding
    • testJwtNoneEvidenceIncludesBaselineStatus — evidence must mention baseline status
    • testJwtNoneAlgorithmDetection — pre-existing test still passes (realistic mock: 401 baseline / 200 with JWT-none)
  • BrokenAuthenticationExtendedTest: 2 new tests + 1 mock fix

    • testExpiredJwtNoFalsePositiveOnPublicEndpoint — same false-positive prevention for expired-JWT test
    • testExpiredJwtAccepted mock updated to be realistic (was: always 200; now: 401 baseline / 200 with auth header)

232/232 tests pass.

Impact

Before this fix, scanning any server that has public endpoints would produce hundreds of spurious CRITICAL findings, making the report useless and undermining trust in ASTF. After this fix, findings are only raised when auth bypass is genuinely demonstrated.

🤖 Generated with Claude Code

GovindarajanL and others added 4 commits May 15, 2026 14:33
Both testJwtNoneAlgorithm and testExpiredJwt flagged CRITICAL/HIGH findings
on public endpoints simply because those endpoints return HTTP 200 to every
request — including one that happens to carry a JWT-none token.  The 200
response was never caused by the token; the endpoint is just public.

Root cause: the tests checked isRequiresAuthentication() (always true for
inline YAML endpoints due to the 2-arg constructor default) but never asked
whether the endpoint actually demanded auth at runtime.

Fix: add a baseline probe (no Authorization header) before each JWT attack
probe.  If the baseline already returns 2xx the endpoint is public and the
JWT test is skipped entirely.  A finding is only raised when the baseline is
4xx/5xx (auth required) but the JWT-attack probe returns 2xx (bypass).

The evidence string now also includes the baseline HTTP status, making scan
reports self-explanatory without needing manual verification.

Tests updated:
- BrokenAuthenticationTestcaseTest: 3 new tests covering the false-positive
  scenario, the true-positive (bypass) scenario, and evidence content
- BrokenAuthenticationExtendedTest: testExpiredJwtAccepted mock updated to
  be realistic (401 baseline / 200 with token); 2 new tests for public-
  endpoint false-positive prevention

All 232 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…false positives)

testAdminEndpointAccess probed 21 hardcoded admin paths (/admin, /config,
/settings, ...) against every endpoint's base URL and flagged CRITICAL when
the response was HTTP 200.

SPAs and reverse proxies return HTTP 200 with text/html for every unknown
path (client-side routing fallback). This caused 26 endpoints × 21 paths =
546 false CRITICAL findings when scanning Rocket.Chat (a React SPA).

Fix: add isApiResponse() helper that checks Content-Type and body sniffing.
Only flag a finding when the response is JSON/XML (a real API response), not
text/html (a SPA shell or error page).

This brings the Rocket.Chat scan from 601 noisy findings down to 51 real,
actionable findings with zero false positives.

All 232 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The -f/--format option had defaultValue = "JSON" which caused the format
field to always be non-null. The buildConfig() guard (if format != null)
therefore always fired and overwrote any outputFormat set in the YAML config
file, making it impossible for users to set the report format via YAML.

Fix: remove defaultValue from the picocli @option so the field is null when
the user does not pass -f explicitly. Priority is now:
  1. -f CLI flag (explicit user override)
  2. outputFormat key in YAML/JSON config file
  3. Default: JSON (applied only when neither of the above is set)

This means outputFormat: "HTML" in a .yaml config now correctly produces an
HTML report without requiring the user to also pass -f HTML on the command
line.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…blic endpoints

Problem
-------
Two false positives remained after the JWT-none and BFLA fixes:

1. "Missing Authentication Controls" fired on /api/info and
   /api/v1/settings.public — both are intentionally public endpoints
   documented as requiring no auth in Rocket.Chat's API. The scanner had
   no way to know this because every inline YAML endpoint defaulted to
   requiresAuthentication=true with no way to override it.

2. "Missing Rate Limiting" fired on those same public endpoints.
   Rate limiting on a public read-only info endpoint is a DoS concern,
   not an auth/data-security concern — not worth flagging in a security
   scan report.

Fix
---
ConfigLoader: parse optional auth/requiresAuthentication field from inline
YAML endpoint blocks:

  - { path: /api/info, method: GET, auth: false }          # short form
  - { path: /api/pub,  method: GET, requiresAuthentication: false }  # long form

Both forms are accepted. When omitted, requiresAuthentication defaults to
true so existing configs are unaffected.

UnrestrictedResourceConsumptionTestCase: skip testRateLimiting entirely
when endpoint.isRequiresAuthentication() is false. Public endpoints are
expected to accept unlimited read requests.

rocketchat-noauth.yaml: mark /api/info, /api/v1/info, and
/api/v1/settings.public as auth: false — these are documented public
endpoints that should not trigger auth-control or rate-limit findings.

Tests: 3 new ConfigLoaderTest cases covering auth:false, long-form alias,
and default-true behaviour. Total: 235/235 passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@GovindarajanL
GovindarajanL merged commit 04f39e7 into OWASP:main May 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant