Skip to content

Commit 6ff0bb3

Browse files
authored
Add some validation for session IDs (#14426)
1 parent 45bad2b commit 6ff0bb3

File tree

3 files changed

+50
-2
lines changed

3 files changed

+50
-2
lines changed

.changeset/quick-eels-join.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-router/node": patch
3+
---
4+
5+
Validate format of incoming session ids

packages/react-router-node/__tests__/sessions-test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,32 @@ describe("File session storage", () => {
5555
expect(session.get("user")).toBeUndefined();
5656
});
5757

58+
it("returns an empty session for invalid session ids", async () => {
59+
let spy = jest.spyOn(console, "warn").mockImplementation(() => {});
60+
let { getSession, commitSession } = createFileSessionStorage({
61+
dir,
62+
});
63+
64+
let cookie = `__session=${btoa(JSON.stringify("0123456789abcdef"))}`;
65+
let session = await getSession(cookie);
66+
session.set("user", "mjackson");
67+
expect(session.get("user")).toBe("mjackson");
68+
let setCookie = await commitSession(session);
69+
session = await getSession(getCookieFromSetCookie(setCookie));
70+
expect(session.get("user")).toBe("mjackson");
71+
72+
cookie = `__session=${btoa(JSON.stringify("0123456789abcdeg"))}`;
73+
session = await getSession(cookie);
74+
session.set("user", "mjackson");
75+
expect(session.get("user")).toBe("mjackson");
76+
debugger;
77+
setCookie = await commitSession(session);
78+
session = await getSession(getCookieFromSetCookie(setCookie));
79+
expect(session.get("user")).toBeUndefined();
80+
81+
spy.mockRestore();
82+
});
83+
5884
it("doesn't destroy the entire session directory when destroying an empty file session", async () => {
5985
let { getSession, destroySession } = createFileSessionStorage({
6086
dir,

packages/react-router-node/sessions/fileStorage.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ export function createFileSessionStorage<Data = SessionData, FlashData = Data>({
4747

4848
try {
4949
let file = getFile(dir, id);
50+
if (!file) {
51+
throw new Error("Error generating session");
52+
}
5053
await fsp.mkdir(path.dirname(file), { recursive: true });
5154
await fsp.writeFile(file, content, { encoding: "utf-8", flag: "wx" });
5255
return id;
@@ -58,6 +61,9 @@ export function createFileSessionStorage<Data = SessionData, FlashData = Data>({
5861
async readData(id) {
5962
try {
6063
let file = getFile(dir, id);
64+
if (!file) {
65+
return null;
66+
}
6167
let content = JSON.parse(await fsp.readFile(file, "utf-8"));
6268
let data = content.data;
6369
let expires =
@@ -81,6 +87,9 @@ export function createFileSessionStorage<Data = SessionData, FlashData = Data>({
8187
async updateData(id, data, expires) {
8288
let content = JSON.stringify({ data, expires });
8389
let file = getFile(dir, id);
90+
if (!file) {
91+
return;
92+
}
8493
await fsp.mkdir(path.dirname(file), { recursive: true });
8594
await fsp.writeFile(file, content, "utf-8");
8695
},
@@ -90,16 +99,24 @@ export function createFileSessionStorage<Data = SessionData, FlashData = Data>({
9099
if (!id) {
91100
return;
92101
}
102+
let file = getFile(dir, id);
103+
if (!file) {
104+
return;
105+
}
93106
try {
94-
await fsp.unlink(getFile(dir, id));
107+
await fsp.unlink(file);
95108
} catch (error: any) {
96109
if (error.code !== "ENOENT") throw error;
97110
}
98111
},
99112
});
100113
}
101114

102-
export function getFile(dir: string, id: string): string {
115+
export function getFile(dir: string, id: string): string | null {
116+
if (!/^[0-9a-f]{16}$/i.test(id)) {
117+
return null;
118+
}
119+
103120
// Divide the session id up into a directory (first 2 bytes) and filename
104121
// (remaining 6 bytes) to reduce the chance of having very large directories,
105122
// which should speed up file access. This is a maximum of 2^16 directories,

0 commit comments

Comments
 (0)