Skip to content

Commit 0f58862

Browse files
committed
Avoid internal encode/decode logic via identity function
1 parent e85393c commit 0f58862

File tree

2 files changed

+80
-66
lines changed

2 files changed

+80
-66
lines changed

packages/react-router/__tests__/server-runtime/cookies-test.ts

Lines changed: 56 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -197,73 +197,78 @@ describe("cookies", () => {
197197
});
198198
});
199199

200-
describe("encode", () => {
201-
it("encodes the cookie using default encoding when no encode function is provided", async () => {
202-
let rawCookieValue = "cookie";
200+
describe("custom encoding/decoding", () => {
201+
it("uses default base64 encoding when no functions are provided", async () => {
202+
let rawCookieValue = "hello world";
203203
let cookie = createCookie("my-cookie");
204204
let setCookie = await cookie.serialize(rawCookieValue);
205-
expect(setCookie).not.toContain("my-cookie=cookie");
205+
expect(setCookie).toContain("my-cookie=ImhlbGxvIHdvcmxkIg%3D%3D;");
206+
let parsed = await cookie.parse(getCookieFromSetCookie(setCookie));
207+
expect(parsed).toBe(rawCookieValue);
206208
});
207209

208-
it("encodes the cookie using the provided encode function at initialization", async () => {
209-
let rawCookieValue = "cookie";
210-
let encodeFn = (str: string) => {
211-
// Check that the value is not encoded before calling encodeFn
212-
expect(str).toBe(rawCookieValue);
213-
return str;
214-
};
215-
let cookie = createCookie("my-cookie", { encode: encodeFn });
210+
it("uses custom implementations when provided at initialization", async () => {
211+
let rawCookieValue = "hello world";
212+
let cookie = createCookie("my-cookie", {
213+
encode(str: string) {
214+
expect(str).toBe(rawCookieValue); // not encoded yet
215+
return encodeURIComponent(str.toUpperCase());
216+
},
217+
decode(str: string) {
218+
expect(str).toBe("HELLO%20WORLD");
219+
return decodeURIComponent(str.toLowerCase());
220+
},
221+
});
216222
let setCookie = await cookie.serialize(rawCookieValue);
217-
expect(setCookie).toContain("my-cookie=cookie");
223+
expect(setCookie).toContain("my-cookie=HELLO%20WORLD;");
224+
let parsed = await cookie.parse(getCookieFromSetCookie(setCookie));
225+
expect(parsed).toBe(rawCookieValue);
218226
});
219227

220-
it("encodes the cookie using the provided encode function when specified during serialization", async () => {
221-
let rawCookieValue = "cookie";
222-
let encodeFn = (str: string) => {
223-
// Check that the value is not encoded before calling encodeFn
224-
expect(str).toBe(rawCookieValue);
225-
return str;
226-
};
228+
it("uses custom implementations when provided at usage time", async () => {
229+
let rawCookieValue = "hello world";
227230
let cookie = createCookie("my-cookie");
228231
let setCookie = await cookie.serialize(rawCookieValue, {
229-
encode: encodeFn,
232+
encode(str: string) {
233+
expect(str).toBe(rawCookieValue); // not encoded yet
234+
return encodeURIComponent(str.toUpperCase());
235+
},
230236
});
231-
expect(setCookie).toContain("my-cookie=cookie");
232-
});
233-
});
234-
235-
describe("decode", () => {
236-
it("decodes cookie using default decode function", async () => {
237-
let rawCookieValue = "cookie";
238-
let cookie = createCookie("my-cookie");
239-
let setCookie = await cookie.serialize(rawCookieValue);
240-
let value = await cookie.parse(getCookieFromSetCookie(setCookie));
241-
expect(value).toBe(rawCookieValue);
237+
expect(setCookie).toContain("my-cookie=HELLO%20WORLD;");
238+
let parsed = await cookie.parse(getCookieFromSetCookie(setCookie), {
239+
decode(str: string) {
240+
expect(str).toBe("HELLO%20WORLD");
241+
return decodeURIComponent(str.toLowerCase());
242+
},
243+
});
244+
expect(parsed).toBe(rawCookieValue);
242245
});
243246

244-
it("decodes cookie using provided encode and decode functions during initialization", async () => {
245-
let rawCookieValue = "cookie";
246-
let encodeFn = (str: string) => str;
247-
let decodeFn = (str: string) => str;
247+
it("uses custom implementations when using signed cookies", async () => {
248+
let rawCookieValue = "hello world";
248249
let cookie = createCookie("my-cookie", {
249-
encode: encodeFn,
250-
decode: decodeFn,
250+
secrets: ["s3cr3t"],
251+
encode(str: string) {
252+
expect(str).toBe(rawCookieValue); // not encoded yet
253+
return encodeURIComponent(str.toUpperCase());
254+
},
255+
decode(str: string) {
256+
expect(str).toBe("HELLO%20WORLD");
257+
return decodeURIComponent(str.toLowerCase());
258+
},
251259
});
252260
let setCookie = await cookie.serialize(rawCookieValue);
253-
let value = await cookie.parse(getCookieFromSetCookie(setCookie));
254-
expect(value).toBe(rawCookieValue);
255-
});
261+
expect(setCookie).toContain(
262+
"my-cookie=HELLO%20WORLD.4bKWgOIqYxcP3KMCHWBmoKEQth3NPQ9yrTRurGMgS40;",
263+
);
264+
let parsed = await cookie.parse(getCookieFromSetCookie(setCookie));
265+
expect(parsed).toBe(rawCookieValue);
256266

257-
it("decodes cookie using provided decode function during parsing", async () => {
258-
let rawCookieValue = "cookie";
259-
let encodeFn = (str: string) => str;
260-
let decodeFn = (str: string) => str;
261-
let cookie = createCookie("my-cookie", { encode: encodeFn });
262-
let setCookie = await cookie.serialize(rawCookieValue);
263-
let value = await cookie.parse(getCookieFromSetCookie(setCookie), {
264-
decode: decodeFn,
265-
});
266-
expect(value).toBe(rawCookieValue);
267+
// Fails if the cookie value is tampered with
268+
parsed = await cookie.parse(
269+
"my-cookie=HELLO%20MARS.4bKWgOIqYxcP3KMCHWBmoKEQth3NPQ9yrTRurGMgS40",
270+
);
271+
expect(parsed).toBe(null);
267272
});
268273
});
269274
});

packages/react-router/lib/server-runtime/cookies.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,17 @@ export const createCookie = (
9797
},
9898
async parse(cookieHeader, parseOptions) {
9999
if (!cookieHeader) return null;
100-
let { decode: decodeFn = decodeData } = { ...options, ...parseOptions };
101-
let cookies = parse(cookieHeader);
100+
let opts = { ...options, ...parseOptions };
101+
let cookies = parse(cookieHeader, {
102+
...opts,
103+
// If they provided a custom decode function, skip internal decoding by
104+
// passing an identity function here
105+
decode: opts.decode ? (v) => v : undefined,
106+
});
102107
if (name in cookies) {
103108
let value = cookies[name];
104109
if (typeof value === "string" && value !== "") {
105-
let decoded = await decodeCookieValue(decodeFn, value, secrets);
110+
let decoded = await decodeCookieValue(value, secrets, opts.decode);
106111
return decoded;
107112
} else {
108113
return "";
@@ -111,16 +116,18 @@ export const createCookie = (
111116
return null;
112117
}
113118
},
114-
async serialize(value, _serializeOptions) {
115-
const { encode: encodeFn = encodeData, ...serializeOptions } = {
116-
...options,
117-
..._serializeOptions,
118-
};
119-
return serialize(
120-
name,
121-
value === "" ? "" : await encodeCookieValue(encodeFn, value, secrets),
122-
serializeOptions,
123-
);
119+
async serialize(value, serializeOptions) {
120+
let opts = { ...options, ...serializeOptions };
121+
let encoded =
122+
value === ""
123+
? ""
124+
: await encodeCookieValue(value, secrets, opts.encode);
125+
return serialize(name, encoded, {
126+
...opts,
127+
// If they provided a custom encode function, skip internal encoding by
128+
// passing an identity function here
129+
encode: opts.encode ? (v) => v : undefined,
130+
});
124131
},
125132
};
126133
};
@@ -143,10 +150,11 @@ export const isCookie: IsCookieFunction = (object): object is Cookie => {
143150
};
144151

145152
async function encodeCookieValue(
146-
encodeFn: (value: string) => string,
147153
value: any,
148154
secrets: string[],
155+
encode: ((value: string) => string) | undefined,
149156
): Promise<string> {
157+
let encodeFn = encode || encodeData;
150158
let encoded = encodeFn(value);
151159

152160
if (secrets.length > 0) {
@@ -157,10 +165,11 @@ async function encodeCookieValue(
157165
}
158166

159167
async function decodeCookieValue(
160-
decodeFn: (value: string) => any,
161168
value: string,
162169
secrets: string[],
170+
decode: ((value: string) => any) | undefined,
163171
): Promise<any> {
172+
let decodeFn = decode ?? decodeData;
164173
if (secrets.length > 0) {
165174
for (let secret of secrets) {
166175
let unsignedValue = await unsign(value, secret);

0 commit comments

Comments
 (0)