Skip to content

Commit 210d457

Browse files
authored
Multipart boundary validation (#16)
2 parents 4413919 + e60c907 commit 210d457

File tree

2 files changed

+81
-1
lines changed

2 files changed

+81
-1
lines changed

src/Multipart.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,56 @@ export class Multipart implements Part {
6767
}
6868

6969
/**
70-
* The boundary bytes used to separate the parts
70+
* Check if the boundary is valid
71+
* A valid boundary is 1 to 70 characters long, does not end with space, and may only contain:
72+
* A-Z a-z 0-9 '()+_,-./:=? and space
73+
*
74+
* ```bnf
75+
* boundary := 0*69<bchars> bcharsnospace
76+
*
77+
* bchars := bcharsnospace / " "
78+
*
79+
* bcharsnospace := DIGIT / ALPHA / "'" / "(" / ")" /
80+
* "+" / "_" / "," / "-" / "." /
81+
* "/" / ":" / "=" / "?"
82+
* ```
83+
*
84+
* From: RFC 2046, Section 5.1.1. Common Syntax
85+
*
86+
* @internal
87+
*/
88+
private static isValidBoundary(boundary: Uint8Array): boolean {
89+
if (boundary.length < 1 || boundary.length > 70 || boundary[boundary.length - 1] === Multipart.SP)
90+
return false;
91+
92+
for (const char of boundary) {
93+
if (char >= 0x30 && char <= 0x39) continue;
94+
if ((char >= 0x41 && char <= 0x5a) || (char >= 0x61 && char <= 0x7a)) continue;
95+
if (
96+
char === Multipart.SP ||
97+
(char >= 0x27 && char <= 0x29) ||
98+
(char >= 0x2b && char <= 0x2f) ||
99+
char === 0x3a ||
100+
char === 0x3d ||
101+
char === 0x3f ||
102+
char === 0x5f
103+
) continue;
104+
return false;
105+
}
106+
107+
return true;
108+
}
109+
110+
/**
111+
* Get the boundary bytes used to separate the parts
71112
*/
72113
public get boundary(): Uint8Array {
73114
return this.#boundary;
74115
}
75116

117+
/**
118+
* Set the boundary bytes used to separate the parts
119+
*/
76120
public set boundary(boundary: Uint8Array | string) {
77121
this.#boundary = typeof boundary === "string" ? new TextEncoder().encode(boundary) : boundary;
78122
this.setHeaders();
@@ -96,8 +140,14 @@ export class Multipart implements Part {
96140
/**
97141
* Get the bytes of the body of this multipart. Includes all parts separated by the boundary.
98142
* Does not include the headers.
143+
*
144+
* @throws {RangeError} If the multipart boundary is invalid. A valid boundary is 1 to 70 characters long,
145+
* does not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space
99146
*/
100147
public get body(): Uint8Array {
148+
if (!Multipart.isValidBoundary(this.#boundary))
149+
throw new RangeError("Invalid boundary: must be 1 to 70 characters long, not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space");
150+
101151
const result: ArrayLike<number>[] = [];
102152
for (const part of this.parts) result.push(Multipart.DOUBLE_DASH, this.boundary, Multipart.CRLF, part.bytes(), Multipart.CRLF);
103153
result.push(Multipart.DOUBLE_DASH, this.boundary, Multipart.DOUBLE_DASH, Multipart.CRLF);
@@ -127,6 +177,9 @@ export class Multipart implements Part {
127177
* @param [mediaType] Multipart media type to pass to the constructor
128178
*/
129179
public static parseBody(data: Uint8Array, boundary: Uint8Array, mediaType?: string): Multipart {
180+
if (!Multipart.isValidBoundary(boundary))
181+
console.warn("Invalid boundary:", new TextDecoder().decode(boundary), "\nMust be 1 to 70 characters long, not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space");
182+
130183
const parts: Uint8Array[] = [];
131184
const fullBoundarySequence = new Uint8Array(Multipart.combineArrays([Multipart.DOUBLE_DASH, boundary, Multipart.CRLF]));
132185
const endBoundarySequence = new Uint8Array(Multipart.combineArrays([Multipart.DOUBLE_DASH, boundary, Multipart.DOUBLE_DASH, Multipart.CRLF]));
@@ -340,6 +393,9 @@ export class Multipart implements Part {
340393

341394
/**
342395
* Get the bytes of the headers and {@link body} of this multipart.
396+
*
397+
* @throws {RangeError} If the multipart boundary is invalid. A valid boundary is 1 to 70 characters long,
398+
* does not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space
343399
*/
344400
public bytes(): Uint8Array {
345401
const result: ArrayLike<number>[] = [];

test/Multipart.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,5 +252,29 @@ describe("Multipart", function () {
252252

253253
expect(new TextDecoder().decode(bytes)).to.equal(new TextDecoder().decode(expectedBytes));
254254
});
255+
256+
it("should accept only valid boundaries", function () {
257+
expect(() => new Multipart([], "").bytes()).to.throw(RangeError, "Invalid boundary");
258+
expect(() => new Multipart([], " ").bytes()).to.throw(RangeError, "Invalid boundary");
259+
expect(() => new Multipart([], "a ").bytes()).to.throw(RangeError, "Invalid boundary");
260+
expect(() => new Multipart([], "0123456789".repeat(7) + "0").bytes()).to.throw(RangeError, "Invalid boundary");
261+
expect(() => new Multipart([], "foo!bar").bytes()).to.throw(RangeError, "Invalid boundary");
262+
263+
expect(() => new Multipart([], "a").bytes()).to.not.throw();
264+
expect(() => new Multipart([], "0123456789".repeat(7)).bytes()).to.not.throw();
265+
expect(() => new Multipart([], "foo bar").bytes()).to.not.throw();
266+
expect(() => new Multipart([], "foo'bar").bytes()).to.not.throw();
267+
expect(() => new Multipart([], "foo(bar").bytes()).to.not.throw();
268+
expect(() => new Multipart([], "foo)bar").bytes()).to.not.throw();
269+
expect(() => new Multipart([], "foo+bar").bytes()).to.not.throw();
270+
expect(() => new Multipart([], "foo_bar").bytes()).to.not.throw();
271+
expect(() => new Multipart([], "foo,bar").bytes()).to.not.throw();
272+
expect(() => new Multipart([], "foo-bar").bytes()).to.not.throw();
273+
expect(() => new Multipart([], "foo.bar").bytes()).to.not.throw();
274+
expect(() => new Multipart([], "foo/bar").bytes()).to.not.throw();
275+
expect(() => new Multipart([], "foo:bar").bytes()).to.not.throw();
276+
expect(() => new Multipart([], "foo=bar").bytes()).to.not.throw();
277+
expect(() => new Multipart([], "foo?bar").bytes()).to.not.throw();
278+
});
255279
});
256280
});

0 commit comments

Comments
 (0)