Skip to content

AppSec audit — nap-java, v0.8.0: findings and remediation order #33

Description

@tcheeric

Scope

Full application-security review of nap-java at v0.8.0: all six modules (nap-core, nap-server, nap-jdbc, nap-client, nap-spring, nap-it), the protocol core, the Spring adapter and its filters, the JDBC stores, dependencies, and CI. Reviewed against the OWASP Top 10 and CWE Top 25, with the NAP v2 RFC as the specification and the TypeScript implementation as the interop reference.

Findings

# Severity Finding
#27 Medium–High Session cookie carries the session id, not the access token
#28 Medium Undocumented proof body-field fallback for the NIP-98 proof
#29 Medium protected-path-prefixes does not fail closed
#30 Medium Auto-configuration defaults fail open (AllowAllAclResolver, in-memory stores)
#31 Medium In-memory stores never evict
#32 Medium No CI at all

Three of these are cross-implementation divergences rather than bugs in isolation, which is what makes them worth treating as a group. nap-java must agree with nap on the wire, and #27 and #28 are places where it does not.

Suggested order

  1. No CI: nothing builds, tests, or scans on a pull request, including the TypeScript interop suite #32 (CI) first, even though it is not the most severe. Nothing else here is verifiable without it, and the existing test suite — official test vectors, TypeScript interop, Postgres round trips — is already strong enough to catch regressions the moment it runs automatically.
  2. Session cookie carries the session id, not the access token: rotation never reaches the credential and session ids are logged #27 — the behavioural fix with real consequences: rotation currently never reaches the credential the browser sends, and session ids are written to logs. Needs a migration note, since it logs existing sessions out once.
  3. protected-path-prefixes does not fail closed: an unannotated handler under a protected prefix serves unauthenticated requests #29 + Auto-configuration defaults fail open: AllowAllAclResolver and in-memory stores are the silent default #30 together — both are one-line default changes plus a CHANGELOG entry, and they address the same theme of failing open.
  4. Undocumented fallback accepts the NIP-98 proof from a proof body field, outside the signed payload hash #28 — deleting the fallback is small; the only real question is whether any client depends on it.
  5. In-memory challenge and session stores never evict: unbounded growth driven by unauthenticated /auth/init #31 — most design work, lowest exploitability.

What the review did not find

  • No injection. Every JdbcSessionStore / JdbcChallengeStore query uses PreparedStatement with bound parameters. findBy(column, value) interpolates a column name, but only from three private call sites passing compile-time constants — not attacker-reachable. Worth an enum for defence in depth, not a finding.
  • The NIP-98 validator is correct, and better than it had to be. verifySignature() recomputes the event id from the canonical serialization before verifying, with a comment explaining precisely why trusting the supplied id would let any note the victim ever published be re-dressed as a completion. That is the subtle bug in this protocol and it is handled.
  • Constant-time comparison throughout. MessageDigest.isEqual for refresh tokens and step-up tokens, including across length differences.
  • Timing side channels handled. padAuthResponse() in a finally so a store outage answers on the same schedule as a refusal; 429s deliberately unpadded.
  • ClientIpResolver is exemplary. The javadoc states the problem (a proxy collapsing every caller into one rate-limit bucket), why the naive fix is worse (client-settable X-Forwarded-For removes the limit entirely), and the right-to-left walk is implemented correctly.
  • EventReplayGuard is bounded, sized to the clock-skew allowance, sweeps without owning a thread, and the unbounded variant is deprecated with an accurate explanation. In-memory challenge and session stores never evict: unbounded growth driven by unauthenticated /auth/init #31 is the same treatment not yet applied to the stores.
  • NapSessionFilter's ACL cache is keyed by principal (not session), bounded, and caches only grants — the reasoning for not caching denials is correct.
  • No secrets committed.

The protocol implementation is careful and the comments explain the reasoning rather than the mechanics, which is what made this productive to audit. Most findings are about defaults and configuration surface rather than the core: the code does the right thing when wired correctly, and does not always insist on being wired correctly.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    securitySecurity-sensitive change

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions