aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-12 12:14:40 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-12 16:36:54 +0200
commitc8462c6060ccd2cccb305edcd64ef12a675f2f58 (patch)
tree03367421c9ece40172dd44a8740e159860dc739f /packages/meshbay-hub/src
parent903ea022e918a05c7c8cb43d95e46d82368566f1 (diff)
downloadmeshbay-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')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/moderation.py19
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.