Skip to content

Commit d5e06c1

Browse files
committed
Use a running count of resolved placeables to protect againt denial of service attacks
1 parent f8cc0f4 commit d5e06c1

File tree

3 files changed

+22
-25
lines changed

3 files changed

+22
-25
lines changed

fluent-bundle/src/resolver.js

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ import { FluentType, FluentNone, FluentNumber, FluentDateTime }
2929
from "./types.js";
3030
import * as builtins from "./builtins.js";
3131

32-
// Prevent expansion of too long placeables.
33-
const MAX_PLACEABLE_LENGTH = 2500;
32+
// The maximum number of placeables which can be expanded in a single call to
33+
// `formatPattern`. The limit protects against the Billion Laughs and Quadratic
34+
// Blowup attacks. See https://msdn.microsoft.com/en-us/magazine/ee335713.aspx.
35+
const MAX_PLACEABLES = 100;
3436

3537
// Unicode bidi isolation characters.
3638
const FSI = "\u2068";
@@ -259,25 +261,24 @@ export function resolveComplexPattern(scope, ptn) {
259261
continue;
260262
}
261263

262-
const part = resolveExpression(scope, elem).toString(scope);
263-
264-
if (useIsolating) {
265-
result.push(FSI);
266-
}
267-
268-
if (part.length > MAX_PLACEABLE_LENGTH) {
264+
scope.placeables++;
265+
if (scope.placeables > MAX_PLACEABLES) {
269266
scope.dirty.delete(ptn);
270267
// This is a fatal error which causes the resolver to instantly bail out
271268
// on this pattern. The length check protects against excessive memory
272269
// usage, and throwing protects against eating up the CPU when long
273270
// placeables are deeply nested.
274271
throw new RangeError(
275-
"Too many characters in placeable " +
276-
`(${part.length}, max allowed is ${MAX_PLACEABLE_LENGTH})`
272+
`Too many placeables expanded: ${scope.placeables}, ` +
273+
`max allowed is ${MAX_PLACEABLES}`
277274
);
278275
}
279276

280-
result.push(part);
277+
if (useIsolating) {
278+
result.push(FSI);
279+
}
280+
281+
result.push(resolveExpression(scope, elem).toString(scope));
281282

282283
if (useIsolating) {
283284
result.push(PDI);

fluent-bundle/src/resource.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@ const TOKEN_COLON = /\s*:\s*/y;
4848
const TOKEN_COMMA = /\s*,?\s*/y;
4949
const TOKEN_BLANK = /\s+/y;
5050

51-
// Maximum number of placeables in a single Pattern to protect against Quadratic
52-
// Blowup attacks. See https://msdn.microsoft.com/en-us/magazine/ee335713.aspx.
53-
const MAX_PLACEABLES = 100;
54-
5551
/**
5652
* Fluent Resource is a structure storing parsed localization entries.
5753
*/
@@ -216,18 +212,13 @@ export default class FluentResource {
216212

217213
// Parse a complex pattern as an array of elements.
218214
function parsePatternElements(elements = [], commonIndent) {
219-
let placeableCount = 0;
220-
221215
while (true) {
222216
if (test(RE_TEXT_RUN)) {
223217
elements.push(match1(RE_TEXT_RUN));
224218
continue;
225219
}
226220

227221
if (source[cursor] === "{") {
228-
if (++placeableCount > MAX_PLACEABLES) {
229-
throw new FluentError("Too many placeables");
230-
}
231222
elements.push(parsePlaceable());
232223
continue;
233224
}

fluent-bundle/src/scope.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ export default class Scope {
33
bundle,
44
errors,
55
args,
6-
insideTermReference = false,
76
dirty = new WeakSet()
87
) {
98
/** The bundle for which the given resolution is happening. */
@@ -13,15 +12,21 @@ export default class Scope {
1312
/** A dict of developer-provided variables. */
1413
this.args = args;
1514

16-
/** Term references require different variable lookup logic. */
17-
this.insideTermReference = insideTermReference;
1815
/** The Set of patterns already encountered during this resolution.
1916
* Used to detect and prevent cyclic resolutions. */
2017
this.dirty = dirty;
18+
/** Term references require different variable lookup logic. */
19+
this.insideTermReference = false;
20+
/** The running count of placeables resolved so far. Used to detect the
21+
* Billion Laughs and Quadratic Blowup attacks. */
22+
this.placeables = 0;
2123
}
2224

2325
cloneForTermReference(args) {
24-
return new Scope(this.bundle, this.errors, args, true, this.dirty);
26+
let scope = new Scope(this.bundle, this.errors, args, this.dirty);
27+
scope.insideTermReference = true;
28+
scope.placeables = this.placeables;
29+
return scope;
2530
}
2631

2732
reportError(error) {

0 commit comments

Comments
 (0)