Skip to content

feat: hide spam stacks from the public list and block offensive aliases - #183

Open
franrolotti wants to merge 1 commit into
developmentfrom
feat/filter-malicious-stacks
Open

feat: hide spam stacks from the public list and block offensive aliases#183
franrolotti wants to merge 1 commit into
developmentfrom
feat/filter-malicious-stacks

Conversation

@franrolotti

Copy link
Copy Markdown
Contributor

Filters maliciously created stacks out of the public list, and stops offensive aliases from being set. Two unrelated abuse vectors that happened to surface together.

Why the circle filter looks like this

SavingCircles.create() is permissionless, and the homepage carousel enumerates every circle id from 0 to nextId — so every spam circle ever created lands on the front page.

Two rules in useAllCircles:

  1. A hand-maintained blocklist of circle ids and owner addresses. Owner is already in circleInfo, so blocking by owner costs no extra RPC and one entry usually covers a spammer's whole batch.
  2. Unstarted circles are hidden from non-members. This is what does the real work — spam circles get created and abandoned, and an unstarted circle can only be joined through an owner-signed invite, so it has nothing to offer someone browsing. The isMember exemption matters: the homepage is only HeroBanner + HomeAllStacks, with no "my stacks" section, so without it you wouldn't see your own stack anywhere on the homepage until you started it.

The blocklist is deliberately not derived from a circle's members. create() takes _circle.owner from calldata and never checks it against msg.sender, so anyone can plant a real user's address as owner of a spam circle. If a block expanded through membership, a spammer could use that to make an innocent user's own circles disappear — they'd control our filter. Deriving the list is also why blocking is applied to the public list only; useUserCirclesList is untouched, so nobody loses circles from their own account page.

Why the alias change looks like this

The offending aliases all passed validateAlias cleanly — the existing rules cover format and impersonation (ASCII-only, so no homoglyphs; no URLs possible) but say nothing about profanity.

  • obscenity handles the bulk. It's zero-dependency, ~27 KB gzipped, and lands in a lazily-loaded chunk — no route's First Load JS changed. It resolved to 0.4.1 rather than latest because of our minimumReleaseAge: 864000 policy in pnpm-workspace.yaml; that's fine, and 0.4.1 actually catches more of the offending names than 0.4.6 does.
  • A short BLOCKED_ALIASES set covers what a wordlist can't. One of the names was an insult only in context, not profanity — nothing automated will ever flag that class, so it needs a list a human can append to.

The profanity check lives only in TS, not mirrored into a Postgres CHECK constraint like RESERVED_ALIASES is. Writes reach profiles solely through POST /api/profile (anon/authenticated writes are revoked), so this is the effective gate, and a wordlist in a constraint would mean a migration every time we add a term.

What this does not do

  • The offensive aliases already in profiles are still live. This only blocks new ones. They render on /stacks/[id] and /account/[address] until someone with Supabase access nulls them:
    update public.profiles set username = null
    where lower(username::text) in ('fuckface','poindexter','vibecoded_bullshite');
    DisplayName falls back to ENS and then a formatted address, so it degrades cleanly.
  • BLOCKED_CIRCLE_IDS and BLOCKED_OWNERS ship empty. The unstarted-circle rule carries the spam side for now; the lists are there to seed once we have specific ids.
  • The create() owner hole is not fixed here. The real fix is require(_circle.owner == msg.sender) in the saving-circles submodule, which needs an upstream PR and a redeploy, and wouldn't clean up existing circles anyway.
  • Filtered pages render fewer than 40 cards, since pagination is driven by nextId. Cosmetic.

Verification

pnpm lint clean, pnpm build succeeds, pnpm format:check clean apart from a pre-existing failure in .github/ISSUE_TEMPLATE/design.md that this branch doesn't touch.

validateAlias was run directly against the real inputs: all three offending aliases are rejected case-insensitively, while alex, bread_lover, assassin and analysis are still accepted (obscenity handles the Scunthorpe class correctly), and the existing length/format/reserved rules are unaffected.

The circle filter was not verified in a browser — the local env points at chain 31337 with no anvil running, and make start-local resets Supabase tables and rewrites .env.local, which didn't seem worth it for two continue statements. Worth a look on a deployed environment before merge.

@netlify

netlify Bot commented Aug 12, 2026

Copy link
Copy Markdown

Deploy Preview for app-stacks ready!

Name Link
🔨 Latest commit 4440c15
🔍 Latest deploy log https://app.netlify.com/projects/app-stacks/deploys/6a7c82ce5bb7d80008cf5ce8
😎 Deploy Preview https://deploy-preview-183--app-stacks.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Two unrelated abuse vectors, both surfacing in the UI:

- create() is permissionless, so anyone can spam circles into the public
  list on the homepage. Adds a hand-maintained blocklist (ids + owners) and
  hides unstarted circles from non-members, since those can only be joined
  through an owner-signed invite anyway.
- Aliases passed validateAlias with offensive names. Adds a profanity check
  plus a short list for names the dataset doesn't recognise.

The blocklist is deliberately not derived from a circle's members: create()
takes the owner from calldata without checking msg.sender, so expanding a
block through membership would let a spammer plant a real user's address on
a spam circle and hide that user's own circles.
@franrolotti
franrolotti force-pushed the feat/filter-malicious-stacks branch from 533df82 to 4440c15 Compare August 12, 2026 14:27
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