Skip to content

Commit 61cda3e

Browse files
committed
test(types): remove reflected-XSS sink from type-test fixtures
CodeQL reported js/reflected-xss (CWE-79, high) against tests/types/catchAsync.test-d.ts: two handler bodies passed `req.params.id` directly to `res.json(...)`. Express serializes a bare string as the entire response body without escaping `<`, which is the risk its `json escape` setting exists to mitigate, so a user-controlled string reaching that sink is a pattern worth removing rather than suppressing. The published package was never affected. The flow existed only in a type-level fixture that is never executed, never registered on a server, and never published, and dist/ is byte-identical before and after. The object form `res.json({ id: ... })` used elsewhere in the same file is not a sink, because reflected XSS requires a string to reach the response body and the object wrapper breaks that flow. Both bodies now assert the parameter typing directly instead of echoing the request. That is a stronger test: the new params-shape assertions pin the custom `Request<P>` type argument, which `res.json(req.params.id)` never did, since a `ParamsDictionary` index signature also yields a string for `.id`.
1 parent 3e137ed commit 61cda3e

2 files changed

Lines changed: 24 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Security
6+
7+
- Removed a reflected-XSS sink (CWE-79) from the type-level test fixtures, reported by CodeQL as `js/reflected-xss`. Two handler bodies passed `req.params.id` straight to `res.json(...)`; Express serializes a bare string as the entire response body without escaping `<`, which is the risk its `json escape` setting exists to mitigate, so a user-controlled string reaching that sink is a genuine pattern to avoid. **The published package was never affected**: the flow existed only in `tests/types/catchAsync.test-d.ts`, a file that is never executed, never registered on a server, and never published (`files: ["dist"]`), and `dist/` is byte-identical before and after this change. The object form `res.json({ id: ... })` used elsewhere in the same file is not a sink, because reflected XSS requires a string to reach the response body. The bodies now assert the parameter typing directly rather than echoing the request, which is a stronger test: the new `params`-shape assertions pin the custom `Request<P>` type argument, which `res.json(req.params.id)` never did, since a `ParamsDictionary` index signature also yields a string for `.id` (`tests/types/catchAsync.test-d.ts`)
8+
59
## [0.7.0] - 2026-07-27
610

711
### Changed

tests/types/catchAsync.test-d.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,26 @@ type _retThrowsAsync = Expect<
165165
* 4. Request / Response type arguments survive the wrapping.
166166
* ------------------------------------------------------------------------- */
167167

168+
// Handler bodies here never write a request value into the response. Passing a
169+
// user-controlled string straight to `res.json(...)` is a reflected-XSS sink
170+
// (CWE-79): Express serializes a bare string as the entire body without escaping
171+
// `<`, which is why Express ships the `json escape` setting. The wrapping is what
172+
// these tests are about, so the parameter typing is asserted directly instead.
168173
const typedParams = catchAsync(
169174
async (rq: Request<{ id: string }>, rs: Response) => {
170-
rs.json(rq.params.id);
175+
void rq;
176+
rs.json({ ok: true });
171177
}
172178
);
173179
type _paramsSurvive = Expect<
174180
Equal<Parameters<typeof typedParams>[0], Request<{ id: string }>>
175181
>;
182+
// Stronger than the old body: this pins the `params` shape itself, which
183+
// `rs.json(rq.params.id)` never did, since a `ParamsDictionary` index signature
184+
// also yields a string for `.id`.
185+
type _paramsShape = Expect<
186+
Equal<Parameters<typeof typedParams>[0]["params"], { id: string }>
187+
>;
176188
const asTypedHandler: RequestHandler<{ id: string }> = typedParams;
177189
void asTypedHandler;
178190
app.get<{ id: string }>("/typed", typedParams);
@@ -182,12 +194,16 @@ interface IdParams {
182194
}
183195
const namedParams = catchAsync(
184196
async (rq: Request<IdParams>, rs: Response) => {
185-
rs.json(rq.params.id);
197+
void rq;
198+
rs.json({ ok: true });
186199
}
187200
);
188201
type _namedSurvive = Expect<
189202
Equal<Parameters<typeof namedParams>[0], Request<IdParams>>
190203
>;
204+
type _namedShape = Expect<
205+
Equal<Parameters<typeof namedParams>[0]["params"], IdParams>
206+
>;
191207

192208
interface AuthedRequest extends Request {
193209
user: { id: string };
@@ -464,7 +480,9 @@ void (0 as unknown as [
464480
_decoratedMethodKeepsSourceArity,
465481
_mitigationHasFullArity,
466482
_paramsSurvive,
483+
_paramsShape,
467484
_namedSurvive,
485+
_namedShape,
468486
_subtypeSurvives,
469487
_errArity,
470488
_errNot3,

0 commit comments

Comments
 (0)