Skip to content

Commit 553443e

Browse files
baozhoutaoclaude
andauthored
fix(grid): validate email format in the import preview (objectstack#3566) (#2840)
ImportWizard's per-cell validateValue did no format check for email columns, so a bad address like `x@柴仟.com` passed client + dry-run validation and only failed at real-import time inside better-auth. - Added isPlausibleEmail (single-pass, ASCII, ReDoS-safe) mirroring the server's isLikelyEmail; email cells are now flagged red in the preview before submit. - Unit test. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 46b811c commit 553443e

3 files changed

Lines changed: 73 additions & 0 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@object-ui/plugin-grid": patch
3+
---
4+
5+
fix(grid): validate email format in the import preview (objectstack#3566)
6+
7+
The ImportWizard's per-cell `validateValue` did no format check for `email`
8+
columns (it fell through to `default → true`), so an obviously-bad address —
9+
e.g. a non-ASCII domain like `x@柴仟.com` — passed client validation (and the
10+
server dry-run) and only failed at real-import time inside better-auth, giving
11+
a jarring "passed validation, then failed" experience.
12+
13+
- Added `isPlausibleEmail`, a single-pass structural + ASCII check that mirrors
14+
the server's `isLikelyEmail`, so bad emails are flagged red in the preview
15+
step before submit. No regex backtracking (same ReDoS-safety as the server).

packages/plugin-grid/src/ImportWizard.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,12 +414,31 @@ function formatDryRunError(
414414
return { fieldLabel, message };
415415
}
416416

417+
/**
418+
* Plausible email? Mirrors the server's `isLikelyEmail` (structure + ASCII) so
419+
* an obviously-bad address — e.g. a non-ASCII domain like `x@柴仟.com` — is
420+
* flagged red in the preview here, instead of passing client + dry-run
421+
* validation only to be rejected by better-auth at real-import time
422+
* (framework#3566). Deliberately not a regex: a single-pass structural check
423+
* has no backtracking (cf. the server-side ReDoS note).
424+
*/
425+
export function isPlausibleEmail(value: string): boolean {
426+
if (value.length === 0 || value.length > 254 || /\s/.test(value)) return false;
427+
if (/[^\x00-\x7f]/.test(value)) return false; // ASCII only, like the server
428+
const at = value.indexOf('@');
429+
if (at <= 0 || at !== value.lastIndexOf('@') || at === value.length - 1) return false;
430+
const domain = value.slice(at + 1);
431+
const dot = domain.lastIndexOf('.');
432+
return dot > 0 && dot < domain.length - 1;
433+
}
434+
417435
function validateValue(value: string, type: string): boolean {
418436
if (!value) return true;
419437
switch (type) {
420438
case 'number': case 'currency': case 'percent': return !isNaN(Number(value));
421439
case 'boolean': return BOOLEAN_IMPORT_TOKENS.has(value.trim().toLowerCase());
422440
case 'date': case 'datetime': return !isNaN(Date.parse(value));
441+
case 'email': return isPlausibleEmail(value.trim());
423442
default: return true;
424443
}
425444
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* ObjectUI – Copyright (c) 2024-present ObjectStack Inc.
3+
* Licensed under MIT.
4+
*/
5+
6+
/**
7+
* isPlausibleEmail() mirrors the server's `isLikelyEmail` (structure + ASCII)
8+
* so obviously-bad addresses fail in the preview step instead of only at
9+
* real-import time (framework#3566).
10+
*/
11+
import { describe, it, expect } from 'vitest';
12+
import { isPlausibleEmail } from './ImportWizard';
13+
14+
describe('isPlausibleEmail', () => {
15+
it('accepts normal addresses', () => {
16+
expect(isPlausibleEmail('name@example.com')).toBe(true);
17+
expect(isPlausibleEmail('first.last@sub.example.co')).toBe(true);
18+
});
19+
20+
it('rejects non-ASCII addresses (the reported bug)', () => {
21+
expect(isPlausibleEmail('735431496@柴仟.com')).toBe(false);
22+
expect(isPlausibleEmail('abc@b.com')).toBe(false);
23+
});
24+
25+
it('rejects structurally-broken addresses', () => {
26+
expect(isPlausibleEmail('no-at.example.com')).toBe(false);
27+
expect(isPlausibleEmail('a@b')).toBe(false);
28+
expect(isPlausibleEmail('a@@b.co')).toBe(false);
29+
expect(isPlausibleEmail('a@.co')).toBe(false);
30+
expect(isPlausibleEmail('a@b.')).toBe(false);
31+
expect(isPlausibleEmail('has space@b.co')).toBe(false);
32+
});
33+
34+
it('is fast on adversarial input (no backtracking)', () => {
35+
const t0 = Date.now();
36+
expect(isPlausibleEmail('!@'.repeat(100_000))).toBe(false);
37+
expect(Date.now() - t0).toBeLessThan(1000);
38+
});
39+
});

0 commit comments

Comments
 (0)