Skip to content

Security fixes for 1.8.15: reset/invite tokens and attachment authorization - #152

Merged
summitt merged 2 commits into
prereleasefrom
security-fixes-1.8.15
Oct 3, 2026
Merged

summitt merged 2 commits into
prereleasefrom
security-fixes-1.8.15

Conversation

@summitt

@summitt summitt commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes the three reports from @Ishbarna (GHSA-3vm2-w3rc-mcgg, GHSA-c6h7-9644-p3pw, GHSA-gx88-52rh-qhcr) and bumps the version to 1.8.15-SNAPSHOT.

  • Misbound invitation token (users API). POST /api/users/addUser with verify=true bound the token to the calling admin; redeeming it set the admin's password. Tokens are now issued by PasswordResets.issue() after the new user is persisted, in the API, the admin Users page and sendReset.

  • Host-header reset links and non-expiring tokens. Links are built by FSUtils.publicBaseUrl() from FACTION_BASE_URL, then FACTION_OAUTH_CALLBACK, and only fall back to the request when neither is set. Tokens expire (1h for resets, 72h for invitations), a new token supersedes the user's earlier ones, and Register redeems via PasswordResets.redeem(), which rejects and deletes expired tokens. Pre-existing rows without an expiry are treated as expiring an hour after creation.

  • IDOR in /service/fileUpload. FileAccess resolves a stored file to its assessment or verification and applies the same rules as opening the assessment (assessor, team, access level; verification assessor or assigned remediation) to download, delete-by-id, delete-by-name and attach. Unauthorized requests get 403, unknown ids 404. Uploads record creatorId.

  • Reset and invitation links never worked when logged out (since 1.8.8). The AccessControlInterceptor only admits unauthenticated requests to the / and /sso namespaces, but Register sat in /portal, so every emailed link redirected to the login page. Register now lives in /, the links point at /Register?uid=..., and RegisterReachabilityTest pins the mapping. This also closes the reachability caveat in GHSA-c6h7.

Verification

  • New tests: PasswordResetsTest (binding, supersession, expiry, legacy rows), PublicBaseUrlTest (configured URL beats Host header), FileAccessTest (two teams, five users, assessment and verification files). Each watched failing first.
  • Full suite: 1049 tests, only the pre-existing EntityUnitTest.testVulnerabilitySection failure.
  • Built app: with a forged Host header, sendReset issued a 1h token for the stranger; the emailed link now opens the registration page without a session, a new password set through it logs in, and the consumed, expired, legacy (no expiry, 2h old) and unknown tokens all show "Link is no longer valid".
  • Built app, two sessions: the stranger gets 403 on download, delete, delete-by-name and attach against the admin's file (404 for an unknown id) while the admin still reads it; an API invite with verify=true produced a token bound to the invitee (user 4), not the admin (user 2), with a 72h expiry; sendReset produced a 1h token.

Deployment note

Set FACTION_BASE_URL (or keep FACTION_OAUTH_CALLBACK set) in any deployment that sends email, so links never derive from the request.

…ization

Three reported issues (GHSA-3vm2-w3rc-mcgg, GHSA-c6h7-9644-p3pw,
GHSA-gx88-52rh-qhcr):

- The users API bound the invitation token to the calling admin instead of
  the new user, so an invitee could set the admin's password. Tokens are now
  issued by PasswordResets.issue() after the target user is persisted, in the
  API, the admin Users page and sendReset.
- Reset and invitation links were built from the request's Host header and
  never expired. FSUtils.publicBaseUrl() builds them from FACTION_BASE_URL,
  then FACTION_OAUTH_CALLBACK, falling back to the request only when neither
  is configured. Tokens carry an expiry (1h reset, 72h invite), a new token
  supersedes the user's earlier ones, and Register redeems through
  PasswordResets.redeem(), which rejects and deletes expired tokens. Rows
  written before this release are treated as expiring an hour after creation.
- /service/fileUpload checked only for a session. FileAccess now resolves a
  stored file to its assessment or verification and applies the same rules
  as opening the assessment (assessor, team, access level, or verification
  assessor/assigned remediation) to download, delete and attach, returning
  403 otherwise and 404 for unknown ids. Uploads record the creator.

Tests: PasswordResetsTest, PublicBaseUrlTest, FileAccessTest.
…ged out

Since 1.8.8 the AccessControlInterceptor only admits unauthenticated requests
to the "/" and "/sso" namespaces, but Register stayed in /portal, so every
password reset and invitation link redirected to the login page. Register now
lives in "/" with login, reset and sendReset, the emailed links point at
/Register, and RegisterReachabilityTest pins the mapping.
@summitt
summitt force-pushed the security-fixes-1.8.15 branch from 776c0c4 to 56ae172 Compare October 3, 2026 06:20
@summitt
summitt changed the base branch from main to prerelease October 3, 2026 06:26
@summitt
summitt merged commit a5fb06e into prerelease Oct 3, 2026
4 of 6 checks 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