Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 38 additions & 25 deletions fluent-bundle/src/resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ import { FluentType, FluentNone, FluentNumber, FluentDateTime }
from "./types.js";
import * as builtins from "./builtins.js";

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

// Unicode bidi isolation characters.
const FSI = "\u2068";
Expand Down Expand Up @@ -86,7 +88,7 @@ function getArguments(scope, args) {
}
}

return [positional, named];
return {positional, named};
}

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

// Resolve a reference to a variable.
function VariableReference(scope, {name}) {
if (!scope.args || !scope.args.hasOwnProperty(name)) {
if (scope.insideTermReference === false) {
scope.reportError(new ReferenceError(`Unknown variable: $${name}`));
let arg;
if (scope.params) {
// We're inside a TermReference. It's OK to reference undefined parameters.
if (scope.params.hasOwnProperty(name)) {
arg = scope.params[name];
} else {
return new FluentNone(`$${name}`);
}
} else if (scope.args && scope.args.hasOwnProperty(name)) {
// We're in the top-level Pattern or inside a MessageReference. Missing
// variables references produce ReferenceErrors.
arg = scope.args[name];
} else {
scope.reportError(new ReferenceError(`Unknown variable: $${name}`));
return new FluentNone(`$${name}`);
}

const arg = scope.args[name];

// Return early if the argument already is an instance of FluentType.
if (arg instanceof FluentType) {
return arg;
Expand Down Expand Up @@ -181,20 +191,23 @@ function TermReference(scope, {name, attr, args}) {
return new FluentNone(id);
}

// Every TermReference has its own variables.
const [, params] = getArguments(scope, args);
const local = scope.cloneForTermReference(params);

if (attr) {
const attribute = term.attributes[attr];
if (attribute) {
return resolvePattern(local, attribute);
// Every TermReference has its own variables.
scope.params = getArguments(scope, args).named;
const resolved = resolvePattern(scope, attribute);
scope.params = null;
return resolved;
}
scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
return new FluentNone(`${id}.${attr}`);
}

return resolvePattern(local, term.value);
scope.params = getArguments(scope, args).named;
const resolved = resolvePattern(scope, term.value);
scope.params = null;
return resolved;
}

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

try {
return func(...getArguments(scope, args));
let resolved = getArguments(scope, args);
return func(resolved.positional, resolved.named);
} catch (err) {
scope.reportError(err);
return new FluentNone(`${name}()`);
Expand Down Expand Up @@ -259,25 +273,24 @@ export function resolveComplexPattern(scope, ptn) {
continue;
}

const part = resolveExpression(scope, elem).toString(scope);

if (useIsolating) {
result.push(FSI);
}

if (part.length > MAX_PLACEABLE_LENGTH) {
scope.placeables++;
if (scope.placeables > MAX_PLACEABLES) {
scope.dirty.delete(ptn);
// This is a fatal error which causes the resolver to instantly bail out
// on this pattern. The length check protects against excessive memory
// usage, and throwing protects against eating up the CPU when long
// placeables are deeply nested.
throw new RangeError(
"Too many characters in placeable " +
`(${part.length}, max allowed is ${MAX_PLACEABLE_LENGTH})`
`Too many placeables expanded: ${scope.placeables}, ` +
`max allowed is ${MAX_PLACEABLES}`
);
}

result.push(part);
if (useIsolating) {
result.push(FSI);
}

result.push(resolveExpression(scope, elem).toString(scope));

if (useIsolating) {
result.push(PDI);
Expand Down
9 changes: 0 additions & 9 deletions fluent-bundle/src/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@ const TOKEN_COLON = /\s*:\s*/y;
const TOKEN_COMMA = /\s*,?\s*/y;
const TOKEN_BLANK = /\s+/y;

// Maximum number of placeables in a single Pattern to protect against Quadratic
// Blowup attacks. See https://msdn.microsoft.com/en-us/magazine/ee335713.aspx.
const MAX_PLACEABLES = 100;

/**
* Fluent Resource is a structure storing parsed localization entries.
*/
Expand Down Expand Up @@ -216,18 +212,13 @@ export default class FluentResource {

// Parse a complex pattern as an array of elements.
function parsePatternElements(elements = [], commonIndent) {
let placeableCount = 0;

while (true) {
if (test(RE_TEXT_RUN)) {
elements.push(match1(RE_TEXT_RUN));
continue;
}

if (source[cursor] === "{") {
if (++placeableCount > MAX_PLACEABLES) {
throw new FluentError("Too many placeables");
}
elements.push(parsePlaceable());
continue;
}
Expand Down
21 changes: 7 additions & 14 deletions fluent-bundle/src/scope.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
export default class Scope {
constructor(
bundle,
errors,
args,
insideTermReference = false,
dirty = new WeakSet()
) {
constructor(bundle, errors, args) {
/** The bundle for which the given resolution is happening. */
this.bundle = bundle;
/** The list of errors collected while resolving. */
this.errors = errors;
/** A dict of developer-provided variables. */
this.args = args;

/** Term references require different variable lookup logic. */
this.insideTermReference = insideTermReference;
/** The Set of patterns already encountered during this resolution.
* Used to detect and prevent cyclic resolutions. */
this.dirty = dirty;
}

cloneForTermReference(args) {
return new Scope(this.bundle, this.errors, args, true, this.dirty);
this.dirty = new WeakSet();
/** A dict of parameters passed to a TermReference. */
this.params = null;
/** The running count of placeables resolved so far. Used to detect the
* Billion Laughs and Quadratic Blowup attacks. */
this.placeables = 0;
}

reportError(error) {
Expand Down