bug: Fix the secuirty issue - #3
Merged
Merged
Conversation
…and pin votes to a stable definition
Addresses a pre-merge security review of the poll feature. Three gaps:
1. Poll access was authorized by nothing but a client-supplied poll UID -
any authenticated (or, if instance policy allowed it, anonymous) caller
could read or vote on a poll embedded in a memo they had no access to,
since the UID alone carried no binding to the memo's visibility or
creator. The REST routes are now nested under the owning memo
(/api/v1/memos/{memoUid}/polls/{pollUid}/votes) and every request runs
through the same server/access read-authorization used for reads
elsewhere (visibility, creator, space membership, anonymous-access
policy) before touching any vote.
2. MySQL had no poll_vote table at all (it was only ever added for SQLite
and PostgreSQL), so every poll operation on a MySQL install would fail
outright. Added store/migration/mysql/0.32/00__poll_vote.sql and the
matching LATEST.sql section, following this directory's existing
conventions (AUTO_INCREMENT, UNIX_TIMESTAMP() defaults, inline KEY
clauses - MySQL has no CREATE INDEX IF NOT EXISTS, unlike SQLite/
Postgres, so indexes have to be inline to keep the migration replay-safe).
3. A poll's definition (question/options/type) lives in the memo's
Markdown, so nothing stopped an edit from reordering or relabeling
options while old votes silently kept pointing at stale indices, or a
```poll block (and its UID) being copied into a second memo to share or
hijack votes. Added a `poll` table binding each UID to the single memo
that first established it (store.EnsurePollBinding): a UID surfacing
under a different memo is rejected (ErrPollMemoMismatch, HTTP 409); a
changed option set/choice-mode under the *same* memo (detected via a
hash of type+options, computed server-side from the memo's live content
by a new Go poll-block parser mirroring the frontend's parsePollDefinition)
clears the now-stale votes and rebinds rather than silently
misattributing them - safe because only someone who could already edit
the memo could have changed its content. poll_vote gained a memo_id
column so memo deletion cleans up both tables the same way every other
memo-child table already does in this codebase (explicit app-code
cleanup in the delete cascade, not FK cascade - SQLite runs with
foreign_keys disabled here).
Frontend: PollBlock/CodeBlock/MemoMarkdownRenderer now thread the owning
memo's resource name down (mirroring how AnchorLink already gets memoName),
since pollApi.ts's requests are memo-scoped; a poll rendered where no memo
context exists (e.g. MemoPreview's relation-embed card) shows statically
with voting disabled rather than erroring. types.ts also now validates a
poll id is uuidv4-shaped, matching a mirrored check server-side, since it
flows unmodified into a URL path segment and this package's SQL layer.
Verified: full go test ./... (including new EnsurePollBinding/cascade
coverage) and the full frontend vitest suite pass; a live end-to-end smoke
test confirmed a non-owner is denied read/vote access to a private memo's
poll (403), anonymous access is denied (401), a copy-pasted poll UID under
a second memo is rejected (409), and editing a poll's options clears its
prior votes. MySQL/PostgreSQL migrations could not be exercised directly
(no Docker daemon in this sandbox for testcontainers) - reviewed by close
cross-reference against this repo's existing migration conventions instead.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016UZX8r5o3xdW8uKDTQv6Mb
…schema An earlier commit added a poll binding table and a memo_id column to poll_vote by editing the already-shipped 0.32/00__poll_vote.sql migration in place, rather than adding a new one. Schema version tracking is a monotonic per-version marker (system_setting), not a hash of each file's content, so a database that had already recorded schema version 0.32.1 from that file's original (poll_vote-only) content never re-ran it - editing an applied migration file has no effect on anyone who already applied it. Every such database was stuck at the old shape while the application code (the memo-deletion cascade, EnsurePollBinding) assumed the new one, so every memo delete failed with "column memo_id does not exist" (Postgres) / the SQLite equivalent - exactly what was reported after upgrading a running instance. Revert postgres/sqlite 0.32/00 to its originally-shipped content (MySQL's 0.32/00 is untouched: it was introduced whole in that same commit, so no MySQL database could have recorded an old-shape version of it) and add a new, strictly later 0.32/01__poll_definition.sql that actually reaches a database sitting at 0.32.1: it creates the poll table and rebuilds poll_vote with memo_id. Existing poll_vote rows predate memo_id entirely - the schema they were written under never tracked which memo a vote's poll belonged to - so there's no data to backfill from; they're discarded (this feature has no production usage yet). The fix rebuilds poll_vote via DROP+CREATE rather than ALTER TABLE ADD COLUMN specifically because a schema-version rollback followed by re-migration is an exercised path in this codebase's own test suite (TestMigrationSpaceMemberStatusBackfillsActive does exactly this to an unrelated table) - a database can already have the new poll_vote shape from LATEST.sql when this file gets replayed, and ADD COLUMN against that errors, which a first attempt at this fix using ALTER TABLE proved by breaking that pre-existing test. Added TestMigrationRepairsPollSchemaAfterInPlaceEdit, which reconstructs the exact stuck state (drops the poll table, drops poll_vote.memo_id, sets schema_version to 0.32.1) and asserts the migration heals it and that deleting a memo with a pre-fix poll_vote row no longer errors. Verified against the exact real-world scenario: built the pre-fix binary (commit e59f859), initialized a database with it (creating a poll_vote row under the old schema, schema_version 0.32.1), then ran the fixed binary against that same data directory. The migration log shows exactly the new 0.32.2 file applying (0.32.1 correctly skipped as already-applied), and deleting the old-schema memo - previously the exact failure - now succeeds. Full go test ./... passes (SQLite; Postgres/MySQL migrations could not be exercised directly - no Docker daemon for testcontainers in this sandbox - reviewed by close reading instead). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_016UZX8r5o3xdW8uKDTQv6Mb
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.
This PR migrates poll voting endpoints from the unnested path /api/v1/polls/{pollUid}/votes to the nested path /api/v1/memos/{memoUid}/polls/{pollUid}/votes (PUT).
Changes Included
testing that was ran
HTTP Request Execution Logs
1. Successful Vote Submission
output
2. Cross-Memo Isolation Check
output
3. Unmapped Legacy Endpoint Verification
output
PostgreSQL Database Verification
Querying the target PostgreSQL database confirms that votes are written to public.poll_vote with the memo_id explicitly linked to Memo 13 (JPU66vU3G8AHsdJkKxPh7W):
Other Note
Let me know what other testing you would like to see.