Skip to content

fix(spam): stop blocking signups whose address is initials then digits - #564

Merged
ralyodio merged 1 commit into
masterfrom
fix/signup-email-shape-false-positive
Sep 22, 2026
Merged

ralyodio merged 1 commit into
masterfrom
fix/signup-email-shape-false-positive

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Reported by email

Someone emailed hello@ugig.net (2026-09-13, subject "Signup blocked by email spam-pattern check") because they could not register. checkEmail refuses their long-established personal Gmail address with "Email matches spam pattern" and offers no appeal route.

They included a minimal reproduction:

checkEmail("abc123456789@example.com")
// => { spam: true, reason: "Email matches spam pattern" }

and noted that #549 changed plus-tag handling but left this rule untouched — which is correct, it is a separate rule.

The rule

/^[a-z]{2,3}\d{6,}@/i,           // ab123456@...

Initials followed by digits is one of the most ordinary ways a real address is formed: a birth year, a phone fragment, digits of pi. The shape alone says nothing about who typed it.

It also fails asymmetrically. A username caught by a shape heuristic can be picked differently; an email address is the one the person has, so a false positive here is a closed door, not an inconvenience.

The fix

Drop that pattern. This is the same correction made twice before in this file:

Each time the conclusion was the same: what marks a generated address is randomness, not a shape. The remaining /^[a-z0-9]{20,}@/i still catches long random local parts, and the disposable-domain set and generated-tag test are untouched — x7f2q9k1m4z8p3w6r5t0@example.com, someone+x7f2q9k1m4z8@gmail.com and someone@mailinator.com are all still blocked.

Tests

The case that asserted ab123456@example.com is spam now asserts it is allowed, alongside the reported shape and three other real-world ones (initials + date of birth, initials + phone fragment, initials + digits of pi). Verified all 14 checkEmail cases behave as asserted.

Notes for follow-up (not in this PR)

  • SPAM_USERNAME_PATTERNS has the same shape rule for usernames (/^[a-z]{2,4}\d{5,}$/i), mirrored in five SQL migrations. Left alone: a username can be chosen differently, and the reporter was blocked on email. Worth a look if signups keep bouncing.
  • The reporter asked whether there is a supported review route for someone whose address trips a rule. There isn't one today — a product call for you rather than a code change.

🤖 Generated with Claude Code

A reporter could not register: checkEmail refused their long-established
personal Gmail address with "Email matches spam pattern" and no appeal route.
They sent a minimal reproduction — checkEmail("abc123456789@example.com")
returns { spam: true } — and noted that PR #549 changed plus-tag handling but
left this rule untouched.

The rule was /^[a-z]{2,3}\d{6,}@/i in SPAM_EMAIL_PATTERNS, meant to catch
ab123456@. Initials followed by digits is one of the most ordinary ways a real
address is formed: a birth year, a phone fragment, digits of pi. The shape
alone says nothing about who typed it, and unlike a username — which a blocked
signup can simply pick differently — an email address is the one they have, so
a false positive here is a closed door rather than an inconvenience.

This is the same correction #549 made to the plus-tag length rule and #531 made
to the username shape heuristics: what marks a generated address is randomness,
not the presence of digits after letters. The remaining /^[a-z0-9]{20,}@/i
still catches long random local parts, and the disposable-domain set and
generated-tag test are untouched.

The test that asserted ab123456@example.com is spam now asserts it is allowed,
alongside the reported shape and three other real-world ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

45 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 8 | LOW: 36

Severity Rule Location
HIGH js-ssrf-outbound-request scripts/scan-all-skills.ts:38
MEDIUM js-open-redirect src/app/agent-login/AgentLoginForm.tsx:38
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/metadata-extract.ts:300
MEDIUM js-dynamic-code-execution src/lib/skills/security-scan.ts:48
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-api-key docs/agents/integration-guide.md:893
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 js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:212
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:223
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:239
LOW secret-generic-credential src/lib/api.test.ts:126
LOW secret-generic-credential src/lib/api.test.ts:131
LOW js-dynamic-code-execution src/lib/skills/composite-scanner.test.ts:106
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:36
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:44
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:66
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:81
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:94
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:103
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:118
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:144
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:162
LOW js-dynamic-code-execution src/lib/skills/security-scan.test.ts:28
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:178
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:191
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.

@ralyodio
ralyodio merged commit 8fdbd3f into master Sep 22, 2026
6 checks passed
@ralyodio
ralyodio deleted the fix/signup-email-shape-false-positive branch September 22, 2026 15:22
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