fix(spam): require corroborating signals before shape heuristics suspend an account - #531
Merged
Merged
Conversation
…name
Shannon entropy scales with length and character diversity, so long
descriptive usernames scored as high as generated ones and were
auto-suspended on signup with no other signal. "vianerds_scoutworkshop"
measured 3.94 against a 3.8 threshold; 143 profiles tripped the rule and
37 of them were readable service names (workbuddy-agent-v2,
hermes_autonomous_agent_090334, sokol-data-pipeline-2026).
Entropy now needs at least one corroborating randomness signal. The
corroborators are weaker than the standalone rules and never flag on
their own -- they only gate the entropy branch. Generated strings are
unbroken tokens that are consonant-heavy, randomly cased, or carry
capital runs; names a human picked use separators or read as words.
'y' is excluded from consonant runs as a semivowel, and the run
threshold is 6, because real compounds reach 5 ("northstar" -> "rthst").
Also realigns spam-check.ts with the SQL thresholds it mirrors -- it had
drifted to >12/>4.0 while check_username_spam() used >10/>3.8 -- so
signup and the is_spam trigger agree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ThreatCrush Security Scan46 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 25 | LOW: 19
Snippets are redacted; ThreatCrush never prints matched credential material. |
Same flaw the entropy rule had: a shape heuristic firing alone on a name
users pick on purpose. "14+ letters with a case-switch ratio above 0.3"
targets generated strings, but CamelCase flips once per word and short
words blow past the threshold -- "TheRealRiotCoder" scores 0.44 and was
suspended for being spelled the way its owner spells it.
Of the 153 names the rule caught, 10 were ordinary CamelCase
(AdaLovelaceBot, SophiaElyaLabs, WatchingMyHuman, JeffGarroRojas).
The rule now skips names that read as deliberate CamelCase: a capital
followed by a lowercase run, repeated -- ^([A-Z][a-z]+)+$. Generated
strings break that shape by starting lowercase ("eMoRPtApRJxcuiGD") or
running capitals together ("OisIHtXmpaUjTVzPmY" -> "IH"). A vowel-ratio
floor backs it up so a random string that happens to fit the shape is
still caught.
Verified against prod: 143 of 153 stay caught, 10 CamelCase names
release, 0 accounts newly suspended.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
What
Two username shape heuristics could suspend an account on their own. Neither can now.
Why
Both fire at signup, on the username string alone, before the account does anything.
profiles.is_spamthen returns403 "Account suspended"from/api/posts, comments, upvote and downvote.1. Shannon entropy. Entropy scales with length and character diversity, so a long descriptive name scores as high as a generated one. The rule's own comment claimed "normal usernames ~3.0-3.5" — but
vianerds_scoutworkshopmeasures 3.94 against a 3.8 threshold. 143 profiles tripped it; 37 were readable service names (workbuddy-agent-v23.95,hermes_autonomous_agent_0903343.90,sokol-data-pipeline-20263.80).2. Mixed case. "14+ letters with a case-switch ratio above 0.3" targets generated strings, but CamelCase flips once per word and short words blow past it.
TheRealRiotCoderscores 0.44 — suspended for being spelled the way its owner spells it. 153 profiles tripped it; 10 were ordinary CamelCase (AdaLovelaceBot,SophiaElyaLabs,WatchingMyHuman,JeffGarroRojas).How
Each rule now needs a second signal. The corroborators are deliberately weaker than the standalone rules and can never flag on their own — they only gate their own branch.
Entropy additionally requires one of:
Mixed case is skipped when the name reads as deliberate CamelCase — a capital followed by a lowercase run, repeated (
^([A-Z][a-z]+)+$), with a vowel-ratio floor so a random string that happens to fit the shape is still caught.The insight both share, taken from the data rather than assumed: generated strings are unbroken tokens that start lowercase (
eMoRPtApRJxcuiGD) or run capitals together (OisIHtXmpaUjTVzPmY->IH); names a human picked use separators or read as words.Two thresholds were tuned against real rows:
yis excluded from consonant runs as a semivowel (counting it madewatchingmyhumana falsengmyhcluster), and the run threshold is 6 because real compounds reach 5 (northstar->rthst).Also
spam-check.tshad drifted from the SQL it mirrors (>12/>4.0vs>10/>3.8). Realigned — this path gates registration outright, so the drift was rejecting legitimate signups.Effect (both migrations already applied to prod)
Brian,sydney,DC1) — storedis_spamcould silently disagree with the current functionBlQyGebwabqMZMmdOp,OisIHtXmpaUjTVzPmY,gfrGEwzqIEDBENAVUhm,eMoRPtApRJxcuiGDTests
40 passing, including 15 new regression cases pinning both directions — descriptive names stay clear, generated ones stay caught.
Known remaining drift
spam-check.tshas no mixed-case rule at all, so signup accepts names the trigger then flags. Not addressed here: adding it would make registration stricter, which is a product call.🤖 Generated with Claude Code