Skip to content

Commit a55c4b2

Browse files
authored
Merge pull request #781 from jonball4/sketchy-codec-protection
fix(superagent-wrapper): avoid mutation by sketchy codecs
2 parents 743e3d9 + d2ac27b commit a55c4b2

File tree

2 files changed

+71
-2
lines changed

2 files changed

+71
-2
lines changed

packages/superagent-wrapper/src/request.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ const patchRequest = <
138138
});
139139
}
140140
return pipe(
141-
route.response[status].decode(res.body),
141+
// deep copy to prevent modification of the inputs by sketchy codecs
142+
route.response[status].decode(structuredClone(res.body)),
142143
E.map((body) =>
143144
decodedResponse<Route>({
144145
status,
@@ -194,7 +195,8 @@ export const requestForRoute =
194195
route: Route,
195196
): BoundRequestFactory<Req, Route> =>
196197
(params: h.RequestType<Route>): PatchedRequest<Req, Route> => {
197-
const reqProps = route.request.encode(params);
198+
// deep copy to prevent modification of the inputs by sketchy codecs
199+
const reqProps = route.request.encode(structuredClone(params));
198200

199201
let path = route.path;
200202
for (const key in reqProps.params) {

packages/superagent-wrapper/test/request.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,37 @@ const PostTestRoute = h.httpRoute({
4545
},
4646
});
4747

48+
const PostOptionalTestRoute = h.httpRoute({
49+
path: '/test/optional/{id}',
50+
method: 'POST',
51+
request: h.httpRequest({
52+
query: {
53+
foo: t.string,
54+
},
55+
params: {
56+
id: NumberFromString,
57+
},
58+
body: {
59+
bar: t.number,
60+
optional: h.optionalized({
61+
baz: t.boolean,
62+
qux: h.optional(t.string),
63+
}),
64+
},
65+
}),
66+
response: {
67+
200: t.type({
68+
id: t.number,
69+
foo: t.string,
70+
bar: t.number,
71+
baz: t.boolean,
72+
}),
73+
401: t.type({
74+
message: t.string,
75+
}),
76+
},
77+
});
78+
4879
const HeaderGetTestRoute = h.httpRoute({
4980
path: '/getHeader',
5081
method: 'GET',
@@ -60,6 +91,9 @@ const TestRoutes = h.apiSpec({
6091
'api.v1.test': {
6192
post: PostTestRoute,
6293
},
94+
'api.v1.test.optional': {
95+
post: PostOptionalTestRoute,
96+
},
6397
'api.v1.getheader': {
6498
get: HeaderGetTestRoute,
6599
},
@@ -105,6 +139,23 @@ const createTestServer = (port: number) => {
105139
}
106140
});
107141

142+
testApp.post('/test/optional/:id', (req, res) => {
143+
const filteredReq = {
144+
query: req.query,
145+
params: req.params,
146+
headers: req.headers,
147+
body: req.body,
148+
};
149+
const params = E.getOrElseW((err) => {
150+
throw new Error(JSON.stringify(err));
151+
})(PostTestRoute.request.decode(filteredReq));
152+
const response = PostTestRoute.response[200].encode({
153+
...params,
154+
baz: true,
155+
});
156+
res.send(response);
157+
});
158+
108159
testApp.get(HeaderGetTestRoute.path, (req, res) => {
109160
res.send(
110161
HeaderGetTestRoute.response[200].encode({
@@ -242,6 +293,22 @@ describe('decodeExpecting', () => {
242293
'Could not decode response 200: [{"invalid":"response"}] due to error [Invalid value undefined supplied to : { id: number, foo: string, bar: number, baz: boolean }/id: number\nInvalid value undefined supplied to : { id: number, foo: string, bar: number, baz: boolean }/foo: string\nInvalid value undefined supplied to : { id: number, foo: string, bar: number, baz: boolean }/bar: number\nInvalid value undefined supplied to : { id: number, foo: string, bar: number, baz: boolean }/baz: boolean]',
243294
);
244295
});
296+
297+
it('does not modify inputs by dropping keys with undefined values', async () => {
298+
const body = {
299+
id: 1337,
300+
foo: 'test',
301+
bar: 42,
302+
optional: { baz: true, qux: undefined },
303+
};
304+
await apiClient['api.v1.test.optional'].post(body).decodeExpecting(200);
305+
assert.deepEqual(body, {
306+
id: 1337,
307+
foo: 'test',
308+
bar: 42,
309+
optional: { baz: true, qux: undefined },
310+
});
311+
});
245312
});
246313

247314
describe('superagent', async () => {

0 commit comments

Comments
 (0)