Skip to content

Commit f3cb69f

Browse files
committed
catch comma and semicolon smugglers
1 parent c283859 commit f3cb69f

2 files changed

Lines changed: 55 additions & 3 deletions

File tree

rest.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ const checkPatchOverrideSupport = function (req, res) {
2626
return undefined !== override && override === "PATCH"
2727
}
2828

29+
/**
30+
* Detects multiple MIME types smuggled into a single Content-Type header.
31+
* Catches both comma-separated types (e.g., "application/json, text/plain")
32+
* and semicolon-smuggled types (e.g., "application/json; text/plain").
33+
* Per RFC 7231/2045, semicolons delimit parameters which must be key=value pairs.
34+
* A segment without '=' after a semicolon is a bare token, not a valid parameter.
35+
*
36+
* @param {string} contentType - Lowercased Content-Type header value
37+
* @returns {boolean} True if multiple MIME types are detected
38+
*/
39+
const hasMultipleContentTypes = (contentType) => {
40+
if (contentType.includes(",")) return true
41+
const segments = contentType.split(";")
42+
return segments.slice(1).some(segment => !segment.trim().includes("="))
43+
}
44+
2945
/**
3046
* Middleware to verify Content-Type headers for endpoints receiving JSON bodies.
3147
* Responds with a 415 Invalid Media Type for Content-Type headers that are not for JSON bodies.
@@ -43,7 +59,7 @@ const verifyJsonContentType = function (req, res, next) {
4359
statusMessage: `Missing or empty Content-Type header.`
4460
}))
4561
}
46-
if (contentType.includes(",")) {
62+
if (hasMultipleContentTypes(contentType)) {
4763
return next(utils.createExpressError({
4864
statusCode: 415,
4965
statusMessage: `Multiple Content-Type values are not allowed. Provide exactly one Content-Type header.`
@@ -73,7 +89,7 @@ const verifyTextContentType = function (req, res, next) {
7389
statusMessage: `Missing or empty Content-Type header.`
7490
}))
7591
}
76-
if (contentType.includes(",")) {
92+
if (hasMultipleContentTypes(contentType)) {
7793
return next(utils.createExpressError({
7894
statusCode: 415,
7995
statusMessage: `Multiple Content-Type values are not allowed. Provide exactly one Content-Type header.`
@@ -103,7 +119,7 @@ const verifyEitherContentType = function (req, res, next) {
103119
statusMessage: `Missing or empty Content-Type header.`
104120
}))
105121
}
106-
if (contentType.includes(",")) {
122+
if (hasMultipleContentTypes(contentType)) {
107123
return next(utils.createExpressError({
108124
statusCode: 415,
109125
statusMessage: `Multiple Content-Type values are not allowed. Provide exactly one Content-Type header.`

routes/__tests__/contentType.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,24 @@ describe("verifyJsonContentType middleware", () => {
156156
expect(response.statusCode).toBe(415)
157157
expect(response.text).toContain("Multiple Content-Type values are not allowed")
158158
})
159+
160+
it("returns 415 for semicolon-smuggled MIME type", async () => {
161+
const response = await request(routeTester)
162+
.post("/json-endpoint")
163+
.set("Content-Type", "application/json; text/plain")
164+
.send('{"test":"data"}')
165+
expect(response.statusCode).toBe(415)
166+
expect(response.text).toContain("Multiple Content-Type values are not allowed")
167+
})
168+
169+
it("returns 415 for semicolon-smuggled MIME type with valid parameter", async () => {
170+
const response = await request(routeTester)
171+
.post("/json-endpoint")
172+
.set("Content-Type", "application/json; charset=utf-8; text/plain")
173+
.send('{"test":"data"}')
174+
expect(response.statusCode).toBe(415)
175+
expect(response.text).toContain("Multiple Content-Type values are not allowed")
176+
})
159177
})
160178

161179
describe("verifyTextContentType middleware", () => {
@@ -204,6 +222,15 @@ describe("verifyTextContentType middleware", () => {
204222
expect(response.statusCode).toBe(415)
205223
expect(response.text).toContain("Multiple Content-Type values are not allowed")
206224
})
225+
226+
it("returns 415 for semicolon-smuggled MIME type", async () => {
227+
const response = await request(routeTester)
228+
.post("/text-endpoint")
229+
.set("Content-Type", "text/plain; application/json")
230+
.send("hello")
231+
expect(response.statusCode).toBe(415)
232+
expect(response.text).toContain("Multiple Content-Type values are not allowed")
233+
})
207234
})
208235

209236
describe("verifyEitherContentType middleware", () => {
@@ -262,4 +289,13 @@ describe("verifyEitherContentType middleware", () => {
262289
expect(response.statusCode).toBe(415)
263290
expect(response.text).toContain("Multiple Content-Type values are not allowed")
264291
})
292+
293+
it("returns 415 for semicolon-smuggled MIME type", async () => {
294+
const response = await request(routeTester)
295+
.post("/either-endpoint")
296+
.set("Content-Type", "application/json; text/plain")
297+
.send('{"test":"data"}')
298+
expect(response.statusCode).toBe(415)
299+
expect(response.text).toContain("Multiple Content-Type values are not allowed")
300+
})
265301
})

0 commit comments

Comments
 (0)