Repository navigation
fix: security review fixes, cookie sessions and Swiss brackets - #10
Merged
Merged
Conversation
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>
Contributor
There was a problem hiding this comment.
🔵 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

No description provided.