Skip to content

Check on the server who may read what, and lock down the REST routes (reads) - #560

Merged
doomlab merged 3 commits into
fix/server-side-access-checksfrom
fix/server-side-read-checks
Oct 9, 2026
Merged

doomlab merged 3 commits into
fix/server-side-access-checksfrom
fix/server-side-read-checks

Conversation

@doomlab

@doomlab doomlab commented Oct 9, 2026

Copy link
Copy Markdown
Member

Stage 2 of closing the server-side access gap found while tracing #539. Stacked on #559, which is stacked on #558: this targets #559's branch; retarget as each merges. Please review the rules below: this is a behaviour change on the server.

Problem

Most read queries took a filter (where/include) straight from the browser, so any signed-in user could read any project's data, and by include-ing task logs could read other people's form responses and task chat. Two classes of thing were worse than a plain read:

  • getUser / getUsers returned the full user row, including hashedPassword, for any user, to any signed-in user. Nothing in the app calls them (leftover scaffolding), so they are deleted.
  • Four REST routes had no sign-in check at all: /api/send-email sent any message from STAPLE's address to anyone (an open mail relay); /api/build-viewer queued build jobs for anyone; /api/viewer-downloads put a URL parameter straight into a file path (a crafted id could read any .zip file on the server).

Rules

Read queries (38 files). A new scopeReadQuery wrapper combines the browser's filter with the caller's own limit, so the filter can only narrow, and hideOthersResponses blanks form responses (metadata) and task chat on any task log the caller may not read, wherever it was included from (a task's logs, a member's logs, a milestone's tasks, tags, the dashboard). Logs the caller may read are: managers' project logs, and logs assigned to the caller or their team. Status, approval and dates stay, so counts and summaries keep working.

Queries Limit
By project id: getProject, getProjectStats, getProjectData, getContributors, project-manager lookups, getUserProjectMemberIds, the three tag-by-project queries Must be a member of the project
getMilestone, getTeam Member of that thing's project
Filter-based: projects, members, tasks, columns, milestones, task log, privileges, tags, team names Only your own projects
getComments Chat only for logs you manage or are assigned to
getRoles / getRole Your own roles, or your projects'
getForms / getForm / getFormDeployments Your own forms
getInvites Invitations for your email, or for projects you manage
getUserWidgets / getProjectWidgets Only your own
Breadcrumb labels Only for things you may see
getUser / getUsers Deleted

REST routes

  • send-email: must be signed in; sender must be STAPLE's address; one recipient who is the caller (profile/password notices), the helpdesk (feedback) or someone the caller invited to a project they manage (invitations).
  • build-viewer: signed in and a manager of at least one project (the Summary page is manager-only in the UI).
  • viewer-downloads (+ head): signed in, and the job id may only contain letters, digits, _ and -, and the path is checked to stay inside the builds folder.

Not covered (follow-ups)

  • sendNotification can still notify arbitrary users (other mutations call it internally, so it needs splitting first).
  • The summary build job itself (viewerWorker) trusts the body it is given; builds are not tied to a specific project or requester, so any manager could fetch another manager's finished summary if they knew its (random) job id.
  • Attaching a role or form version to a task isn't checked against ownership.

Testing

  • queryAccess.test.ts (51 tests): id-based queries refuse outsiders (and plain contributors where it isn't theirs), members are not refused, every filter-based query's database call carries the caller's limit alongside the browser's filter, and responses/chat pulled in through include are blanked (manager sees all). 37 of them fail against the old queries.
  • apiRoutesAccess.test.ts (9 tests): sign-in, allowed and refused emails, manager-only build, traversal ids. 6 fail against the old routes.
  • Typecheck (apart from the existing getJsonSchema.test.ts errors), lint and the whole suite (269 tests) pass.
  • Not yet checked in the app: please try as a project manager and as a contributor: dashboard and widgets, the task board, a task page, milestones and Gantt, the contributors and teams pages, tags, roles, forms, invites (send and accept), the profile and password emails, feedback, and building and downloading a project summary.

🤖 Generated with Claude Code

doomlab and others added 3 commits October 9, 2026 01:58
…r's filter

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…uild-viewer, viewer-downloads)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@doomlab
doomlab merged commit 25accf6 into fix/server-side-access-checks Oct 9, 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