Skip to content

fix(spam): require corroborating signals before shape heuristics suspend an account - #531

Merged
ralyodio merged 2 commits into
masterfrom
fix/entropy-needs-corroboration
Aug 16, 2026
Merged

fix(spam): require corroborating signals before shape heuristics suspend an account#531
ralyodio merged 2 commits into
masterfrom
fix/entropy-needs-corroboration

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

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_spam then returns 403 "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_scoutworkshop measures 3.94 against a 3.8 threshold. 143 profiles tripped it; 37 were readable service names (workbuddy-agent-v2 3.95, hermes_autonomous_agent_090334 3.90, sokol-data-pipeline-2026 3.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. TheRealRiotCoder scores 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:

  • no separator and case-switch ratio > 0.25 (randomly cased token)
  • no separator and vowel ratio < 0.25 (consonant-heavy token)
  • max consonant run >= 6 (unpronounceable cluster)
  • no separator and has lowercase and capital run >= 3 (capital run mid-token)

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: y is excluded from consonant runs as a semivowel (counting it made watchingmyhuman a false ngmyh cluster), and the run threshold is 6 because real compounds reach 5 (northstar -> rthst).

Also

spam-check.ts had drifted from the SQL it mirrors (>12/>4.0 vs >10/>3.8). Realigned — this path gates registration outright, so the drift was rejecting legitimate signups.

Effect (both migrations already applied to prod)

  • Flagged profiles: 248 -> 172
  • 0 accounts newly suspended by either change, verified by diffing against a pre-migration snapshot
  • 67 released by the entropy fix: 37 false positives + 30 stale flags from an older function version that no re-backfill ever corrected (Brian, sydney, DC1) — stored is_spam could silently disagree with the current function
  • 9 released by the mixed-case fix; 143 of 153 stay caught
  • Bot strings still caught, verified: BlQyGebwabqMZMmdOp, OisIHtXmpaUjTVzPmY, gfrGEwzqIEDBENAVUhm, eMoRPtApRJxcuiGD

Tests

40 passing, including 15 new regression cases pinning both directions — descriptive names stay clear, generated ones stay caught.

Known remaining drift

spam-check.ts has 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

…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>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

46 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 25 | LOW: 19

Severity Rule Location
HIGH secret-generic-api-key docs/agents/integration-guide.md:893
HIGH js-ssrf-outbound-request scripts/scan-all-skills.ts:38
MEDIUM manifest-install-lifecycle-script package.json:18
MEDIUM js-open-redirect src/app/agent-login/AgentLoginForm.tsx:38
MEDIUM js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:212
MEDIUM js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:223
MEDIUM js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:239
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:79
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:90
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:113
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:144
MEDIUM js-open-redirect src/components/funding/FundingClient.tsx:137
MEDIUM js-dynamic-code-execution src/lib/skills/composite-scanner.test.ts:106
MEDIUM js-dynamic-code-execution src/lib/skills/metadata-extract.ts:300
MEDIUM js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:36
MEDIUM js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:44
MEDIUM js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:66
MEDIUM js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:81
MEDIUM js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:94
MEDIUM js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:103
MEDIUM js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:118
MEDIUM js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:144
MEDIUM js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:162
MEDIUM js-dynamic-code-execution src/lib/skills/security-scan.test.ts:28
MEDIUM js-dynamic-code-execution src/lib/skills/security-scan.ts:48
MEDIUM js-dynamic-code-execution src/lib/skills/url-import.test.ts:178
MEDIUM js-dynamic-code-execution src/lib/skills/url-import.test.ts:191
LOW secret-generic-credential cli/src/commands/auth.test.ts:66
LOW secret-generic-credential cli/src/commands/auth.test.ts:85
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:53
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:68
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:87
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:158
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:182
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:193
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:232
LOW secret-generic-credential src/lib/api.test.ts:126
LOW secret-generic-credential src/lib/api.test.ts:131
LOW secret-generic-credential src/lib/validations.test.ts:148
LOW secret-generic-credential src/lib/validations.test.ts:512
LOW secret-generic-credential src/lib/validations.test.ts:523
LOW secret-generic-credential src/lib/validations.test.ts:538
LOW secret-generic-credential src/lib/validations.test.ts:548
LOW secret-generic-credential src/lib/validations.test.ts:557
LOW secret-generic-credential src/lib/validations.test.ts:567
LOW secret-generic-credential src/lib/validations.test.ts:582

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>
@ralyodio ralyodio changed the title fix(spam): require a corroborating signal before entropy flags a username fix(spam): require corroborating signals before shape heuristics suspend an account Aug 16, 2026
@ralyodio
ralyodio merged commit 6173242 into master Aug 16, 2026
6 checks passed
@ralyodio
ralyodio deleted the fix/entropy-needs-corroboration branch August 16, 2026 10:49
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