Skip to content

Commit e6896ea

Browse files
open-design-crew[bot]NJUHua
andauthored
fix(landing): reject injection-scanner emails at the contact-sales boundary (#5863)
07-11 an SQLi scanner submitted ~19 payload emails like testing@example.com'||dbms_pipe.receive_message(...)||' to /contact-sales. The loose EMAIL_RE (no whitespace, one @, a dot) accepted them into the CONTACT_LEADS KV namespace, and the downstream Bitable leads sync — whose email column rejects such values inside an atomic batch insert — failed on every run for six days, silently stalling 30+ real leads. Tighten EMAIL_RE: dotted alphanumeric domain labels with a letters-only TLD; local part keeps the common atom charset including apostrophes (o'brien@…). Red spec first: the new test replays the captured scanner payloads (red on the old regex) and pins real-lead shapes as passing. Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
1 parent f13ed2c commit e6896ea

2 files changed

Lines changed: 34 additions & 1 deletion

File tree

apps/landing-page/functions/contact-sales.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,15 @@ const ALLOWED_ORIGINS = [
6767
"http://127.0.0.1",
6868
];
6969

70-
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
70+
// Strict shape, not the loose "no whitespace, one @, a dot" check: the 07-11
71+
// SQLi scan submitted payloads like `testing@example.com'||dbms_pipe...` that
72+
// the loose regex accepted into KV and which then broke the downstream Bitable
73+
// sync (its email column rejects them, and the batch insert is atomic).
74+
// Local part: common atom charset incl. apostrophe (o'brien@…). Domain:
75+
// hyphenated alphanumeric labels with a letters-only TLD — no quotes, pipes,
76+
// parens, or `&`/`=` can survive this.
77+
const EMAIL_RE =
78+
/^[A-Za-z0-9._%+'-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*\.[A-Za-z]{2,}$/;
7179
const MAX_EMAIL_LENGTH = 254;
7280
const MAX_SHORT = 200;
7381
const MAX_MESSAGE = 4000;

apps/landing-page/tests/contact-sales.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,31 @@ describe("contact-sales validation", () => {
4949
assert.equal((await call({ name: "Ada", source: "pricing_team" })).body.error, "invalid_email");
5050
});
5151

52+
it("rejects injection-scanner emails that are whitespace-free but not a real address (2026-07-11 incident)", async () => {
53+
// Real payloads captured from the 07-11 SQLi scan of this endpoint. All
54+
// passed the old loose regex (one @, a dot, no whitespace), landed in KV,
55+
// and broke the downstream Bitable sync for six days.
56+
const payloads = [
57+
"testing@example.com'||dbms_pipe.receive_message(chr(98)||chr(98)||chr(98),15)||'",
58+
'testing@example.com"&&sleep(27*1000)*nogzzf&&"',
59+
"testing@example.com0'xor(if(now()=sysdate(),sleep(15),0))xor'z",
60+
"testing@example.com&n964235=v916916",
61+
"testing@example.com9408701", // digits-only junk appended to the TLD
62+
"../testing@example.com",
63+
"testing@example.com'\"",
64+
];
65+
for (const email of payloads) {
66+
const { status, body } = await call({ ...ENTERPRISE_OK, email });
67+
assert.equal(status, 400, `should reject ${email}`);
68+
assert.equal(body.error, "invalid_email", `should reject ${email}`);
69+
}
70+
// Legitimate shapes must keep passing, including an apostrophe local part.
71+
assert.equal((await call({ ...ENTERPRISE_OK, email: "o'brien@acme.ie" })).status, 200);
72+
assert.equal((await call({ ...ENTERPRISE_OK, email: "w.vince.0202@gmail.com" })).status, 200);
73+
assert.equal((await call({ ...ENTERPRISE_OK, email: "bingjunxiang.sd@chinatelecom.cn" })).status, 200);
74+
assert.equal((await call({ ...ENTERPRISE_OK, email: "rodrigo@linkflow.com.br" })).status, 200);
75+
});
76+
5277
it("does not require a name on the shared lead-form sources; the in-app `client` source still does", async () => {
5378
// Neither web surface collects a name (email is the contact handle).
5479
assert.equal((await call(ENTERPRISE_OK)).status, 200);

0 commit comments

Comments
 (0)