Skip to content

Commit 70d6520

Browse files
authored
Merge pull request #439 from stasm/lol
Use a running count of resolved placeables to protect against denial of service attacks
2 parents fb423e0 + 43e37c0 commit 70d6520

File tree

3 files changed

+45
-48
lines changed

3 files changed

+45
-48
lines changed

fluent-bundle/src/resolver.js

Lines changed: 38 additions & 25 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";
@@ -86,7 +88,7 @@ function getArguments(scope, args) {
8688
}
8789
}
8890

89-
return [positional, named];
91+
return {positional, named};
9092
}
9193

9294
// Resolve an expression to a Fluent type.
@@ -115,15 +117,23 @@ function resolveExpression(scope, expr) {
115117

116118
// Resolve a reference to a variable.
117119
function VariableReference(scope, {name}) {
118-
if (!scope.args || !scope.args.hasOwnProperty(name)) {
119-
if (scope.insideTermReference === false) {
120-
scope.reportError(new ReferenceError(`Unknown variable: $${name}`));
120+
let arg;
121+
if (scope.params) {
122+
// We're inside a TermReference. It's OK to reference undefined parameters.
123+
if (scope.params.hasOwnProperty(name)) {
124+
arg = scope.params[name];
125+
} else {
126+
return new FluentNone(`$${name}`);
121127
}
128+
} else if (scope.args && scope.args.hasOwnProperty(name)) {
129+
// We're in the top-level Pattern or inside a MessageReference. Missing
130+
// variables references produce ReferenceErrors.
131+
arg = scope.args[name];
132+
} else {
133+
scope.reportError(new ReferenceError(`Unknown variable: $${name}`));
122134
return new FluentNone(`$${name}`);
123135
}
124136

125-
const arg = scope.args[name];
126-
127137
// Return early if the argument already is an instance of FluentType.
128138
if (arg instanceof FluentType) {
129139
return arg;
@@ -181,20 +191,23 @@ function TermReference(scope, {name, attr, args}) {
181191
return new FluentNone(id);
182192
}
183193

184-
// Every TermReference has its own variables.
185-
const [, params] = getArguments(scope, args);
186-
const local = scope.cloneForTermReference(params);
187-
188194
if (attr) {
189195
const attribute = term.attributes[attr];
190196
if (attribute) {
191-
return resolvePattern(local, attribute);
197+
// Every TermReference has its own variables.
198+
scope.params = getArguments(scope, args).named;
199+
const resolved = resolvePattern(scope, attribute);
200+
scope.params = null;
201+
return resolved;
192202
}
193203
scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
194204
return new FluentNone(`${id}.${attr}`);
195205
}
196206

197-
return resolvePattern(local, term.value);
207+
scope.params = getArguments(scope, args).named;
208+
const resolved = resolvePattern(scope, term.value);
209+
scope.params = null;
210+
return resolved;
198211
}
199212

200213
// Resolve a call to a Function with positional and key-value arguments.
@@ -213,7 +226,8 @@ function FunctionReference(scope, {name, args}) {
213226
}
214227

215228
try {
216-
return func(...getArguments(scope, args));
229+
let resolved = getArguments(scope, args);
230+
return func(resolved.positional, resolved.named);
217231
} catch (err) {
218232
scope.reportError(err);
219233
return new FluentNone(`${name}()`);
@@ -259,25 +273,24 @@ export function resolveComplexPattern(scope, ptn) {
259273
continue;
260274
}
261275

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) {
276+
scope.placeables++;
277+
if (scope.placeables > MAX_PLACEABLES) {
269278
scope.dirty.delete(ptn);
270279
// This is a fatal error which causes the resolver to instantly bail out
271280
// on this pattern. The length check protects against excessive memory
272281
// usage, and throwing protects against eating up the CPU when long
273282
// placeables are deeply nested.
274283
throw new RangeError(
275-
"Too many characters in placeable " +
276-
`(${part.length}, max allowed is ${MAX_PLACEABLE_LENGTH})`
284+
`Too many placeables expanded: ${scope.placeables}, ` +
285+
`max allowed is ${MAX_PLACEABLES}`
277286
);
278287
}
279288

280-
result.push(part);
289+
if (useIsolating) {
290+
result.push(FSI);
291+
}
292+
293+
result.push(resolveExpression(scope, elem).toString(scope));
281294

282295
if (useIsolating) {
283296
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: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
11
export default class Scope {
2-
constructor(
3-
bundle,
4-
errors,
5-
args,
6-
insideTermReference = false,
7-
dirty = new WeakSet()
8-
) {
2+
constructor(bundle, errors, args) {
93
/** The bundle for which the given resolution is happening. */
104
this.bundle = bundle;
115
/** The list of errors collected while resolving. */
126
this.errors = errors;
137
/** A dict of developer-provided variables. */
148
this.args = args;
159

16-
/** Term references require different variable lookup logic. */
17-
this.insideTermReference = insideTermReference;
1810
/** The Set of patterns already encountered during this resolution.
1911
* Used to detect and prevent cyclic resolutions. */
20-
this.dirty = dirty;
21-
}
22-
23-
cloneForTermReference(args) {
24-
return new Scope(this.bundle, this.errors, args, true, this.dirty);
12+
this.dirty = new WeakSet();
13+
/** A dict of parameters passed to a TermReference. */
14+
this.params = null;
15+
/** The running count of placeables resolved so far. Used to detect the
16+
* Billion Laughs and Quadratic Blowup attacks. */
17+
this.placeables = 0;
2518
}
2619

2720
reportError(error) {

0 commit comments

Comments
 (0)