Skip to content

Stacked on #288: review hardening (quiet autosave, no PII/private-repo leaks, safer sanitizer, upload gate) - #289

Merged
gregv merged 1 commit into
developfrom
pr288-review-fixes
Sep 29, 2026
Merged

gregv merged 1 commit into
developfrom
pr288-review-fixes

Conversation

@gregv

@gregv gregv commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Stacked on #288. The base is develop, which is #288's head branch, so merging this updates #288 automatically. Review it as the hardening pass on #288.

Fixes the highest-risk problems found reviewing #288. Each has a regression test written first (and seen failing).

Changes

Problem in #288 Fix
Autosave floods Slack and flushes caches. The dashboard autosaves every ~1.5s; each save made a blocking, no-timeout Slack audit post and flushed every hackathon cache. save_project no longer audits; it flushes caches only on the first (legacy -> draft) save. submit_project still audits.
Admin PropelAuth ID on a public doc. reminders_sent.by stored the admin's ID on the hackathon doc, which the unauthenticated event endpoint serves. by is "admin"/"cron" (the real actor stays in the private Slack audit). reminders_sent is stripped from the public event and list payloads (_PRIVATE_HACKATHON_FIELDS).
sanitize_markdown corrupted prose and code. const onSubmit = () => save() became const => save(); "online = true" lost words. on*= / href= / src= scrubbing now runs only inside HTML-tag spans. Attack strings (<img alt="a>b" onerror=...>, <svg/onload=...>, ...) are still stripped.
GET /api/github/activity (unauthenticated) could read private repos (commit messages, authors, PR counts) via the shared token. Private repos raise PrivateRepoError before any commit/PR call and return the same 404 as not-found. Team repos are created public, so nothing legitimate changes.
The teams/<id>/ CDN prefix was not access-controlled. /api/messages/upload-image accepted any directory from any logged-in user, so a stranger could overwrite another team's project thumbnail (the validator trusts that prefix). authorize_team_upload_directory: teams/<id>/... uploads need team membership or admin; .. and bare teams are rejected. Other directories unchanged. Adds ~8 lines to messages_views.upload_image; only the project editor uploads under teams/.
Reminder cron fragility. A naive/"Z" stored deadline raised (500 for the whole run); a total Slack outage still recorded the idempotency key; one bad event aborted the others. Deadline is normalized (bad value -> 409 invalid_deadline); no key recorded when nothing was delivered; per-event failures are isolated, but the endpoint returns 500 so the hourly GitHub job still goes red.

Also adds a short "don't regress" section to CLAUDE.md.

Tests

CI does not run pytest (the step is commented out in main.yml), so these were run locally: 224 passed across api/submissions api/peer_votes api/github api/messages/tests api/teams/tests (plus api/judging/tests, api/volunteers/tests, test/common/utils/test_validators.py). New tests: test_submissions_service.py (autosave quiet, reminders, upload gate), test_validators.py (prose kept / attacks still scrubbed), test_github_activity.py (private repo), test_hackathon_public_payload.py, test_upload_image_gate.py (route-level 403/200/unaffected).

Test plan

  1. /hack/<event>/manageteam as a team member: type in the story for ~10s. It autosaves ("Saved") and the audit Slack channel gets no project_save posts. Submit still posts one project_submit audit.
  2. Save a story containing const onSubmit = () => save() and "our app is online = true": it round-trips intact on /hack/<event>/team/<id>.
  3. Upload a project thumbnail as a member (works). As a non-member, POST /api/messages/upload-image with directory=teams/<other-team>/project returns 403 not_team_member.
  4. GET /api/messages/hackathon/<event> after sending a reminder from ?section=deadlines: response has no reminders_sent. Firestore doc shows by: "admin".
  5. GET /api/github/activity?org=<org>&repo=<a-private-repo>: 404 repo_not_found; a public team repo still returns activity.

Not included (listed on #288)

Re-publish leaving the old Hackers' Choice award, publish not requiring voting closed, the ballot/void write race, activity-endpoint rate limiting, and the 1-hour reminder window vs hourly cron.

🤖 Generated with Claude Code

…r sanitizer, upload gate

- save_project (autosave hot path): drop the per-save Slack audit and only
  flush hackathon caches on the first (legacy -> draft) save.
- Reminders: never write the admin's PropelAuth id onto the hackathon doc;
  strip reminders_sent from the public event + list payloads; normalize the
  stored deadline (naive/"Z" no longer 500s the cron); don't record the
  idempotency key when nothing was delivered; isolate per-event failures but
  keep the hourly job red (HTTP 500) when one fails.
- sanitize_markdown: scope on*=/href=/src= scrubbing to HTML-tag spans so
  prose and code (`const onSubmit = ...`) is no longer silently corrupted.
- GET /api/github/activity: refuse private repos (404, same as not-found).
- POST /api/messages/upload-image: teams/<id>/ directories are writable only
  by that team's members (or admins); reject `..` and bare `teams`.

Regression tests added for each.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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