Skip to content

fix: security review fixes, cookie sessions and Swiss brackets - #10

Merged
tormachris merged 4 commits into
masterfrom
fix/security-review
Oct 9, 2026
Merged

tormachris merged 4 commits into
masterfrom
fix/security-review

Conversation

@tormachris

Copy link
Copy Markdown
Member

No description provided.

tormachris and others added 3 commits October 8, 2026 18:50
Logins and token checks looked users up with ILIKE, so `%` and `_` in an
email acted as wildcards. With self-registration on, anyone could register
`%@example.org` and receive a token that resolved to another account at that
domain. Emails are now matched exactly, on the ix_users_email_lower index.

Authentication
- The JWT subject is the user id, and every token carries a hash that changes
  with the password, so a password change signs out the other sessions.
- The frontend's session is an HttpOnly, SameSite=Strict cookie, __Host-
  prefixed in production. Requests that change data with it need an
  X-Requested-With header (CSRF), and POST /logout removes it. API clients
  can still send the bearer token.
- Any invalid token is a 401 instead of a 500, unknown emails are checked
  against a dummy hash so that logins take as long either way, and changing
  the password needs the current one.

Authorization and validation
- Stage items and their inputs can't reference another tournament's stage
  items or rankings, ranking routes are scoped to their tournament, and
  archived tournaments can't gain or lose stage items.
- Names, scores, durations and the Details link are validated. Bulk imports
  report invalid names instead of failing with a 500, and link the players
  to their teams.
- A duplicate email is a 400, another user's account a 403, and a missing
  player or team a 404.

Removed
- The /static mount, which served whatever directory was mounted on it.
- The unused upcoming_matches endpoint, whose iterations parameter could
  exhaust the server's memory, along with its suggestion search.
- fastapi-sso, gunicorn and the deprecated uvicorn worker. The image runs
  uvicorn as a user that can't modify the code.
- The API docs in production, which now sends a deny-all CSP. A replaced
  logo is deleted once the change is committed.

Every fix has a test, the stale tests pass again, and the row count helper
now asserts the count it is given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ackets

Sessions
- The session token is an HttpOnly cookie, so scripts can't read it. The
  page keeps only the signed-in user's name and id, logs out through the API
  and removes the tokens that older versions left in localStorage.
- Changing the password asks for the current one.
- The dev server proxies /api, so the page and the API share an origin, as
  in production.

Content-Security-Policy
- nginx sends a strict CSP (no inline scripts, no eval), frame denial,
  nosniff, HSTS, and cache headers that never serve a stale index.html. Its
  config is a template that the image's own entrypoint fills in.
- The fonts are self-hosted with Fontsource instead of loaded from Google
  Fonts, and Zod runs without eval.

Bundle
- Organizer pages, forms, Zod and brackets-viewer load on demand, and React
  has a chunk of its own: visitors download 578 KB instead of 936 KB, and a
  release changes 263 KB of it.
- The bracket theme loads with brackets-viewer's stylesheet, after it.

Brackets
- The Bracket tab also draws Swiss stages, as a one-group round robin with
  the rounds side by side. The ranking stays on Standings.
- Nothing in a bracket reacts to hover except the team names, which open
  the team pages.

Also: large tournaments load all their players and teams, and the bulk team
import explains its "team, players" format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Logos persist in a named volume, instead of ./backend being mounted over
  the public /static path. Postgres has a healthcheck, and Redis is pinned
  to a major version.
- FORWARDED_ALLOW_IPS passes through, so that rate limits can count per
  client behind the reverse proxy (see the README).
- CI also runs ruff check, mypy on the tests, the test suite against
  Postgres and the frontend build. pnpm's version comes from package.json.
- The README covers the reverse proxy, sessions, uploads and the tests.
- The stray root package.json and lockfile are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 8, 2026 18:18
@tormachris tormachris changed the title A security and code-quality review of the backend, frontend, containers and CI, with the fixes. It also moves the session token into an HttpOnly cookie, splits the frontend bundle, and draws Swiss stages on the public Bracket tab. fix: security review fixes, cookie sessions and Swiss brackets Oct 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The broad authentication, authorization, deployment, and persistence changes warrant final human security and operational review.

1 open finding
What changed in this PR

This PR hardens authentication, authorization, uploads, validation, deployment, and CI while adding Swiss bracket visualization and frontend performance improvements.

Changes:

  • Replaces browser token storage with HttpOnly cookie sessions and CSRF protection.
  • Adds stricter validation, authorization boundaries, upload persistence, and security headers.
  • Adds Swiss bracket views, code splitting, self-hosted fonts, and broader automated testing.
File Description
README.md Documents deployment, sessions, and testing.
pnpm-lock.yaml Removes obsolete root lockfile.
package.json Removes obsolete root package.
frontend/​vite.config.ts Adds proxying and chunk splitting.
frontend/​src/​zod-config.ts Disables CSP-incompatible Zod JIT.
frontend/​src/​styles.css Updates fonts and relocates bracket styles.
frontend/​src/​openapi/​zod.gen.ts Updates generated validation schemas.
frontend/​src/​openapi/​types.gen.ts Updates generated API types.
frontend/​src/​openapi/​index.ts Updates generated exports.
frontend/​src/​openapi/​@tanstack/​react-query.gen.ts Updates generated query helpers.
frontend/​src/​main.tsx Configures API, fonts, and legacy-session cleanup.
frontend/​src/​app/​utils.ts Adds Swiss bracket detection and same-origin API defaults.
frontend/​src/​app/​types.ts Adapts session and viewer types.
frontend/​src/​app/​sections/​tournament-standings.tsx Extracts standings UI.
frontend/​src/​app/​sections/​tournament-schedule.tsx Extracts schedule UI.
frontend/​src/​app/​sections/​tournament-roster.tsx Clarifies bulk-team input.
frontend/​src/​app/​sections/​tournament-management.tsx Removes extracted sections.
frontend/​src/​app/​sections/​tournament-bracket.tsx Displays Swiss rounds.
frontend/​src/​app/​pages/​user-page.tsx Requires current password.
frontend/​src/​app/​pages/​tournament-page.tsx Adds lazy loading and Swiss navigation.
frontend/​src/​app/​pages/​new-tournament-form.tsx Extracts tournament creation form.
frontend/​src/​app/​pages/​home-page.tsx Lazy-loads creation form.
frontend/​src/​app/​pages/​auth-pages.tsx Stores session identity only.
frontend/​src/​app/​hooks.ts Changes local session metadata key.
frontend/​src/​app/​forms.ts Centralizes date-time validation.
frontend/​src/​app/​components/​bracket-viewer.tsx Lazy-loads viewer implementation.
frontend/​src/​app/​components/​bracket-viewer-canvas.tsx Hosts viewer rendering logic.
frontend/​src/​app/​components/​bracket-viewer-canvas.css Provides deferred viewer styling.
frontend/​src/​app/​bracket-adapter.ts Adapts Swiss rounds for the viewer.
frontend/​src/​app/​api.ts Configures cookie authentication and larger pages.
frontend/​src/​app.tsx Adds logout and route code splitting.
frontend/​pnpm-lock.yaml Locks self-hosted font dependencies.
frontend/​package.json Adds fonts and package metadata.
frontend/​nginx/​templates/​default.conf.template Adds caching and security headers.
frontend/​nginx/​default.conf Removes superseded nginx configuration.
frontend/​index.html Removes third-party fonts.
frontend/​Dockerfile Improves build caching and entrypoint integration.
frontend/​docker-entrypoint.sh Removes superseded entrypoint.
frontend/​docker-entrypoint.d/​05-bracket-runtime-config.envsh Generates runtime and CSP configuration.
frontend/​.dockerignore Updates ignored build output.
docker-compose.yml Adds upload persistence and health checks.
backend/​uv.lock Updates backend dependency lock.
backend/​tests/​unit_tests/​swiss_test.py Removes obsolete pairing tests.
backend/​tests/​unit_tests/​swiss_pairing_test.py Updates input typing.
backend/​tests/​integration_tests/​uploads.py Adds asynchronous removal helper.
backend/​tests/​integration_tests/​sql.py Enforces row-count assertions.
backend/​tests/​integration_tests/​mocks.py Updates token creation.
backend/​tests/​integration_tests/​api/​users_test.py Tests account security changes.
backend/​tests/​integration_tests/​api/​tournaments_test.py Tests links and upload removal.
backend/​tests/​integration_tests/​api/​teams_test.py Tests bulk teams and uploads.
backend/​tests/​integration_tests/​api/​stages_test.py Corrects stage assertions.
backend/​tests/​integration_tests/​api/​stage_items_test.py Tests ownership and archive restrictions.
backend/​tests/​integration_tests/​api/​rankings_test.py Tests ranking ownership.
backend/​tests/​integration_tests/​api/​players_test.py Tests bulk-player validation.
backend/​tests/​integration_tests/​api/​matches_test.py Removes obsolete suggestion tests.
backend/​tests/​integration_tests/​api/​inputs_test.py Tests cross-tournament rejection.
backend/​tests/​integration_tests/​api/​clubs_test.py Ensures unique test clubs.
backend/​tests/​integration_tests/​api/​auth_test.py Tests JWT, cookies, and CSRF.
backend/​static/​.gitignore Removes obsolete static directory marker.
backend/​pyproject.toml Removes unused runtime dependencies.
backend/​Dockerfile Runs Uvicorn with writable uploads only.
backend/​bracket/​uvicorn.py Removes obsolete Gunicorn worker.
backend/​bracket/​utils/​pydantic.py Adds shared name and link validation.
backend/​bracket/​utils/​pagination.py Raises maximum page size.
backend/​bracket/​utils/​errors.py Maps lowercase-email uniqueness errors.
backend/​bracket/​utils/​db_init.py Relies on schema-managed index creation.
backend/​bracket/​sql/​validation.py Validates ranking ownership.
backend/​bracket/​sql/​users.py Hardens access and email queries.
backend/​bracket/​sql/​stage_item_inputs.py Scopes ranking lookups by tournament.
backend/​bracket/​sql/​players.py Returns inserted player IDs.
backend/​bracket/​schema.py Defines lowercase-email index.
backend/​bracket/​routes/​util.py Adds tournament-scoped ranking dependency.
backend/​bracket/​routes/​users.py Secures account and password updates.
backend/​bracket/​routes/​tournaments.py Hardens creation and upload handling.
backend/​bracket/​routes/​teams.py Adds player membership and upload cleanup.
backend/​bracket/​routes/​stage_items.py Protects archived tournaments.
backend/​bracket/​routes/​stage_item_inputs.py Enforces stage-item ownership.
backend/​bracket/​routes/​rankings.py Scopes ranking mutations.
backend/​bracket/​routes/​players.py Improves validation and missing-player handling.
backend/​bracket/​routes/​models.py Removes obsolete suggestion response.
backend/​bracket/​routes/​matches.py Removes obsolete suggestion endpoint.
backend/​bracket/​routes/​auth.py Implements hardened JWT cookie sessions.
backend/​bracket/​models/​db/​user.py Adds constrained names and password confirmation.
backend/​bracket/​models/​db/​tournament.py Validates names and detail links.
backend/​bracket/​models/​db/​team.py Validates and parses bulk teams.
backend/​bracket/​models/​db/​stage.py Constrains stage names.
backend/​bracket/​models/​db/​stage_item.py Constrains stage-item names.
backend/​bracket/​models/​db/​round.py Constrains round names.
backend/​bracket/​models/​db/​player.py Validates bulk player names.
backend/​bracket/​models/​db/​match.py Validates scores and timing.
backend/​bracket/​models/​db/​club.py Constrains club names.
backend/​bracket/​logic/​scheduling/​upcoming_matches.py Removes obsolete suggestion logic.
backend/​bracket/​logic/​scheduling/​shared.py Removes obsolete scheduling helpers.
backend/​bracket/​logic/​scheduling/​ladder_teams.py Removes obsolete ladder pairing logic.
backend/​bracket/​config.py Removes obsolete SSO configuration.
backend/​bracket/​app.py Adds global CSRF and production hardening.
.gitignore Ignores uploaded files.
.github/​workflows/​static-checks.yml Adds database-backed tests and builds.
Files not reviewed (2)
  • frontend/pnpm-lock.yaml: Generated file
  • pnpm-lock.yaml: Generated file

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +22 to +23
# Styles may be inline because the toasts inject theirs at runtime. Scripts may not, as the
# session token is in localStorage, where any injected script could read it.
The comment on the nginx security headers still said that the session token
is in localStorage, where any script could read it. The session is an
HttpOnly cookie now. Inline scripts stay forbidden, because an injected
script could still send requests with the cookie and act as the organizer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tormachris
tormachris merged commit 61efc4b into master Oct 9, 2026
8 checks passed
@tormachris
tormachris deleted the fix/security-review branch October 9, 2026 07:25
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.

2 participants