feat: hide spam stacks from the public list and block offensive aliases - #183
Open
franrolotti wants to merge 1 commit into
Open
feat: hide spam stacks from the public list and block offensive aliases#183franrolotti wants to merge 1 commit into
franrolotti wants to merge 1 commit into
Conversation
✅ Deploy Preview for app-stacks ready!
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
force-pushed
the
feat/filter-malicious-stacks
branch
from
August 12, 2026 14:27
533df82 to
4440c15
Compare
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.
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 from0tonextId— so every spam circle ever created lands on the front page.Two rules in
useAllCircles:circleInfo, so blocking by owner costs no extra RPC and one entry usually covers a spammer's whole batch.isMemberexemption matters: the homepage is onlyHeroBanner + 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.ownerfrom calldata and never checks it againstmsg.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;useUserCirclesListis untouched, so nobody loses circles from their own account page.Why the alias change looks like this
The offending aliases all passed
validateAliascleanly — the existing rules cover format and impersonation (ASCII-only, so no homoglyphs; no URLs possible) but say nothing about profanity.obscenityhandles 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 ourminimumReleaseAge: 864000policy inpnpm-workspace.yaml; that's fine, and 0.4.1 actually catches more of the offending names than 0.4.6 does.BLOCKED_ALIASESset 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_ALIASESis. Writes reachprofilessolely throughPOST /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
profilesare still live. This only blocks new ones. They render on/stacks/[id]and/account/[address]until someone with Supabase access nulls them:DisplayNamefalls back to ENS and then a formatted address, so it degrades cleanly.BLOCKED_CIRCLE_IDSandBLOCKED_OWNERSship empty. The unstarted-circle rule carries the spam side for now; the lists are there to seed once we have specific ids.create()owner hole is not fixed here. The real fix isrequire(_circle.owner == msg.sender)in thesaving-circlessubmodule, which needs an upstream PR and a redeploy, and wouldn't clean up existing circles anyway.nextId. Cosmetic.Verification
pnpm lintclean,pnpm buildsucceeds,pnpm format:checkclean apart from a pre-existing failure in.github/ISSUE_TEMPLATE/design.mdthat this branch doesn't touch.validateAliaswas run directly against the real inputs: all three offending aliases are rejected case-insensitively, whilealex,bread_lover,assassinandanalysisare still accepted (obscenityhandles 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
31337with no anvil running, andmake start-localresets Supabase tables and rewrites.env.local, which didn't seem worth it for twocontinuestatements. Worth a look on a deployed environment before merge.