Skip to content

Commit cd6ccfd

Browse files
committedJul 1, 2022
fix(core): handle invalid email
1 parent 89d91ea commit cd6ccfd

File tree

4 files changed

+101
-21
lines changed

4 files changed

+101
-21
lines changed
 

‎packages/next-auth/src/core/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SessionStore } from "./lib/cookie"
99
import type { NextAuthAction, NextAuthOptions } from "./types"
1010
import type { Cookie } from "./lib/cookie"
1111
import type { ErrorType } from "./pages/error"
12+
import { parse as parseCookie } from "cookie"
1213

1314
export interface RequestInternal {
1415
/** @default "http://localhost:3000" */
@@ -68,7 +69,7 @@ async function toInternalRequest(
6869
method: req.method,
6970
headers,
7071
body: await getBody(req),
71-
cookies: {},
72+
cookies: parseCookie(req.headers.get("cookie")),
7273
providerId: nextauth[1],
7374
error: url.searchParams.get("error") ?? nextauth[1],
7475
host: detectHost(headers["x-forwarded-host"] ?? headers.host),

‎packages/next-auth/src/core/routes/signin.ts

+19-6
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,25 @@ export default async function signin(params: {
3030
return { redirect: `${url}/error?error=OAuthSignin` }
3131
}
3232
} else if (provider.type === "email") {
33-
// Note: Technically the part of the email address local mailbox element
34-
// (everything before the @ symbol) should be treated as 'case sensitive'
35-
// according to RFC 2821, but in practice this causes more problems than
36-
// it solves. We treat email addresses as all lower case. If anyone
37-
// complains about this we can make strict RFC 2821 compliance an option.
38-
const email = body?.email?.toLowerCase() ?? null
33+
/**
34+
* @note Technically the part of the email address local mailbox element
35+
* (everything before the @ symbol) should be treated as 'case sensitive'
36+
* according to RFC 2821, but in practice this causes more problems than
37+
* it solves. We treat email addresses as all lower case. If anyone
38+
* complains about this we can make strict RFC 2821 compliance an option.
39+
*/
40+
let email = body?.email?.toLowerCase()
41+
42+
if (!email) return { redirect: `${url}/error?error=EmailSignin` }
43+
44+
email = email
45+
.split(",")[0]
46+
.trim()
47+
.replaceAll("&", "&")
48+
.replaceAll("<", "&lt;")
49+
.replaceAll(">", "&gt;")
50+
.replaceAll('"', "&quot;")
51+
.replaceAll("'", "&#x27;")
3952

4053
// Verified in `assertConfig`
4154
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { createCSRF, handler } from "./lib"
2+
import EmailProvider from "../src/providers/email"
3+
4+
const originalEmail = "balazs@email.com"
5+
6+
test.each([
7+
[originalEmail, `,<a href="example.com">Click here!</a>`],
8+
[originalEmail, ""],
9+
])("Sanitize email", async (emailOriginal, emailCompromised) => {
10+
const sendEmail = jest.fn()
11+
12+
const { secret, csrf } = createCSRF()
13+
14+
const email = {
15+
original: emailOriginal,
16+
compromised: `${emailOriginal}${emailCompromised}`,
17+
}
18+
19+
const { res } = await handler(
20+
{
21+
providers: [EmailProvider({ sendVerificationRequest: sendEmail })],
22+
adapter: {
23+
getUserByEmail: (email) => ({ id: "1", email, emailVerified: null }),
24+
createVerificationToken: (token) => token,
25+
} as any,
26+
secret,
27+
},
28+
{
29+
prod: true,
30+
path: "signin/email",
31+
requestInit: {
32+
method: "POST",
33+
body: JSON.stringify({
34+
email: email.compromised,
35+
csrfToken: csrf.value,
36+
}),
37+
headers: { "Content-Type": "application/json", Cookie: csrf.cookie },
38+
},
39+
}
40+
)
41+
42+
if (!emailCompromised) {
43+
expect(res.redirect).toBe(
44+
"http://localhost:3000/api/auth/verify-request?provider=email&type=email"
45+
)
46+
expect(sendEmail).toHaveBeenCalledWith(
47+
expect.objectContaining({
48+
identifier: email.original,
49+
token: expect.any(String),
50+
})
51+
)
52+
} else {
53+
expect(res.redirect).not.toContain("error=EmailSignin")
54+
55+
const emailTo = sendEmail.mock.calls[0][0].identifier
56+
expect(emailTo).not.toBe(email.compromised)
57+
expect(emailTo).toBe(email.original)
58+
}
59+
})

‎packages/next-auth/tests/lib.ts

+21-14
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createHash } from "crypto"
12
import type { LoggerInstance, NextAuthOptions } from "../src"
23
import { NextAuthHandler } from "../src/core"
34

@@ -7,17 +8,16 @@ export const mockLogger: () => LoggerInstance = () => ({
78
debug: jest.fn(() => {}),
89
})
910

11+
interface HandlerOptions {
12+
prod?: boolean
13+
path?: string
14+
params?: URLSearchParams | Record<string, string>
15+
requestInit?: RequestInit
16+
}
17+
1018
export async function handler(
1119
options: NextAuthOptions,
12-
{
13-
prod,
14-
path,
15-
params,
16-
}: {
17-
prod?: boolean
18-
path?: string
19-
params?: URLSearchParams | Record<string, string>
20-
}
20+
{ prod, path, params, requestInit }: HandlerOptions
2121
) {
2222
// @ts-ignore
2323
if (prod) process.env.NODE_ENV = "production"
@@ -27,11 +27,7 @@ export async function handler(
2727
params ?? {}
2828
)}`
2929
)
30-
const req = new Request(url, {
31-
headers: {
32-
host: "",
33-
},
34-
})
30+
const req = new Request(url, { headers: { host: "" }, ...requestInit })
3531
const logger = mockLogger()
3632
const response = await NextAuthHandler({
3733
req,
@@ -49,3 +45,14 @@ export async function handler(
4945
log: logger,
5046
}
5147
}
48+
49+
export function createCSRF() {
50+
const secret = "secret"
51+
const value = "csrf"
52+
const token = createHash("sha256").update(`${value}${secret}`).digest("hex")
53+
54+
return {
55+
secret,
56+
csrf: { value, token, cookie: `next-auth.csrf-token=${value}|${token}` },
57+
}
58+
}

0 commit comments

Comments
 (0)
Please sign in to comment.