diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-12 12:14:40 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-12 16:36:54 +0200 |
| commit | c8462c6060ccd2cccb305edcd64ef12a675f2f58 (patch) | |
| tree | 03367421c9ece40172dd44a8740e159860dc739f /packages/meshbay-hub/src/meshbay_hub/api | |
| parent | 903ea022e918a05c7c8cb43d95e46d82368566f1 (diff) | |
| download | meshbay-c8462c6060ccd2cccb305edcd64ef12a675f2f58.tar.gz | |
fix(hub): bound the blocklist reads a stranger can make
`GET /v1/blocklist` takes no authentication — a node syncs it at startup —
and had no ceiling on `limit`, so anyone could ask for the table in one query,
repeatedly. `GET /v1/blocklist/check` took any string of any length straight
into a primary-key lookup, unmetered.
Not changed, and worth a decision rather than a quiet edit:
`AUTO_BLOCK_THRESHOLD` is 3. Three distinct accounts blocking a hash adds it
to the list every node enforces, network-wide, automatically, with manual
admin removal the only undo. Far better than the anonymous version it
replaced, and still a censorship primitive an attacker buys for three email
addresses. §13.5b records the option — count only accounts more than a day
old, which costs a patient attacker a day and an honest reporter nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/moderation.py | 19 |
1 files changed, 16 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/moderation.py b/packages/meshbay-hub/src/meshbay_hub/api/moderation.py index 0939eec..ee10cbc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/moderation.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/moderation.py @@ -25,7 +25,7 @@ Node integration: import logging from datetime import datetime, timezone -from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi import APIRouter, Depends, HTTPException, Query, Request from pydantic import BaseModel from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession @@ -138,11 +138,20 @@ async def report_content( @router.get("/v1/blocklist/check") +@limiter.limit("120/minute") async def check_blocklist( hash: str, + request: Request, db: AsyncSession = Depends(get_db), ): - """Check if a single hash is blocked. Used by nodes before serving public content.""" + """Check if a single hash is blocked. Used by nodes before serving public content. + + Unauthenticated, because a node consults it before serving public content + and does so on its own behalf. That makes the shape check worth having: + without it any string of any length became a primary-key lookup. + """ + if len(hash) != 64 or not all(c in "0123456789abcdef" for c in hash): + raise HTTPException(status_code=422, detail="hash must be 64 hex chars (blake3)") blocked = await db.get(ContentBlocklist, hash) return { "blocked": blocked is not None, @@ -154,7 +163,11 @@ async def check_blocklist( @router.get("/v1/blocklist") async def get_blocklist( db: AsyncSession = Depends(get_db), - limit: int = 10000, + # Bounded, like every other list. This one takes no authentication — a + # node syncs it at startup — and had no ceiling at all, so any stranger + # could ask for the table in one query, repeatedly. 10 000 is what a node + # asks for, so it is the default and also the most anyone may have. + limit: int = Query(default=10000, ge=1, le=10000), ): """ Return the full blocklist. Nodes sync this on startup. |