Thanks to visit codestin.com
Credit goes to github.com

Skip to content

bug: Fix the secuirty issue - #3

Merged
Novapixel1010 merged 2 commits into
pollsfrom
poll-patch
Sep 2, 2026
Merged

bug: Fix the secuirty issue#3
Novapixel1010 merged 2 commits into
pollsfrom
poll-patch

Conversation

@Novapixel1010

Copy link
Copy Markdown
Owner

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

  • Endpoint Relocation & Unmapping: Removed the legacy unnested route /api/v1/polls/{pollUid}/votes and registered the PUT handler under /api/v1/memos/{memoUid}/polls/{pollUid}/votes.
  • Cross-Memo Isolation: Enforced strict relational scoping. If a poll UID is submitted under a memo UID to which it does not belong, the request is rejected immediately with a 404 ("poll not found in memo").
  • Ballot Management: Payload processing now consumes optionIndexes []int32, replacing existing user choices in poll_vote in a single transaction to support ballot updates without duplicate rows.

testing that was ran

HTTP Request Execution Logs

1. Successful Vote Submission

curl -i -X PUT "https://notes2.in.com/api/v1/memos/JPU66vU3G8AHsdJkKxPh7W/polls/dde9eaa9-c3bc-4c55-a8b6-3a4461f012da/votes" \
 -H "Authorization: Bearer memos_pat_ZD2ytDFIqRE1iWUMvJTVAUkGUEXezVtm" \
 -H "Content-Type: application/json" \
 -d '{"optionIndexes": [0]}'

output

HTTP/2 200 
alt-svc: h3=":443"; ma=2592000
content-type: application/json
date: Wed, 02 Sep 2026 11:55:54 GMT
vary: Origin
via: 1.1 Caddy
content-length: 83

{"votes":[{"optionIndex":0,"voter":"users/mike"}],"currentVoterName":"users/mike"}

2. Cross-Memo Isolation Check

curl -i -X PUT "https://notes2.in.com/api/v1/memos/oSMZmui2tDz8ouUc2weqGx/polls/dde9eaa9-c3bc-4c55-a8b6-3a4461f012da/votes" \
  -H "Authorization: Bearer memos_pat_ZD2ytDFIqRE1iWUMvJTVAUkGUEXezVtm" \
  -H "Content-Type: application/json" \
  -d '{"optionIndexes": [0]}'

output

HTTP/2 404 
alt-svc: h3=":443"; ma=2592000
content-type: application/json
date: Wed, 02 Sep 2026 12:25:00 GMT
vary: Origin
via: 1.1 Caddy
content-length: 35

{"error":"poll not found in memo"}

3. Unmapped Legacy Endpoint Verification

curl -i -X PUT "https://notes2.in.com/api/v1/polls/dde9eaa9-c3bc-4c55-a8b6-3a4461f012da/votes" \
  -H "Authorization: Bearer memos_pat_ZD2ytDFIqRE1iWUMvJTVAUkGUEXezVtm" \
  -H "Content-Type: application/json" \
  -d '{"optionIndexes": [0]}'

output

HTTP/2 404 
alt-svc: h3=":443"; ma=2592000
content-type: application/json
date: Wed, 02 Sep 2026 12:25:38 GMT
vary: Origin
via: 1.1 Caddy
content-length: 45

{"code":5,"message":"Not Found","details":[]}

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):

notes3=> SELECT id, created_ts, poll_uid, memo_id, option_index, voter_id 
FROM public.poll_vote 
WHERE poll_uid = 'dde9eaa9-c3bc-4c55-a8b6-3a4461f012da';
 id | created_ts |               poll_uid               | memo_id | option_index | voter_id 
----+------------+--------------------------------------+---------+--------------+----------
  7 | 1788350210 | dde9eaa9-c3bc-4c55-a8b6-3a4461f012da |      13 |            0 |        1
(1 row)

notes3=> SELECT id, uid FROM public.memo WHERE id = 13;
 id |          uid           
----+------------------------
 13 | JPU66vU3G8AHsdJkKxPh7W
(1 row)
  1. confirm the database is their
notes3=> \dt
              List of relations
 Schema |      Name      | Type  |   Owner    
--------+----------------+-------+------------
 public | attachment     | table | memosuser2
 public | idp            | table | memosuser2
 public | inbox          | table | memosuser2
 public | memo           | table | memosuser2
 public | memo_relation  | table | memosuser2
 public | memo_share     | table | memosuser2
 public | poll           | table | memosuser2
 public | poll_vote      | table | memosuser2
 public | reaction       | table | memosuser2
 public | space          | table | memosuser2
 public | space_member   | table | memosuser2
 public | system_setting | table | memosuser2
 public | user           | table | memosuser2
 public | user_identity  | table | memosuser2
 public | user_setting   | table | memosuser2
(15 rows)
  1. checked the memos id and visibility
notes3=> SELECT id, uid, creator_id, space_id, visibility FROM public.memo WHERE id = 13;
 id |          uid           | creator_id | space_id | visibility 
----+------------------------+------------+----------+------------
 13 | JPU66vU3G8AHsdJkKxPh7W |          1 |          | PUBLIC
(1 row)

Other Note
Let me know what other testing you would like to see.

…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
@Novapixel1010 Novapixel1010 added the bug Something isn't working label Sep 2, 2026
@Novapixel1010
Novapixel1010 merged commit b3f95ec into polls Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants