[breaking] trust setting to indicate whether input text is trusted (#1794)

* trust option to indicate whether input text is trusted

* Revamp into trust contexts beyond just command

* Document new trust function style

* Fix screenshot testing

* Use trust setting in \url and \href

* Check `isTrusted` in `\url` and `\href` (so now disabled by default)
* Automatically compute `protocol` from `url` in `isTrusted`, so it
  doesn't need to be passed into every context.

* Document untrusted features in support list/table

* Existing tests trust by default

* remove allowedProtocols and fix flow errors

* remove 'allowedProtocols' from documentation

* add a comment about a flow error, rename urlToProtocol to protocolFromUrl

* add tests test that use function version of trust option

* default trust to false in MathML tests

* fix test title, remove 'trust: false' from test settings since it's the default
This commit is contained in:
Erik Demaine
2019-07-08 21:57:23 -04:00
committed by Kevin Barabash
parent fc79f79c78
commit 3800dc49c1
16 changed files with 352 additions and 62 deletions

View File

@@ -7,14 +7,14 @@ import {validUnit} from "./units";
import {supportedCodepoint} from "./unicodeScripts";
import unicodeAccents from "./unicodeAccents";
import unicodeSymbols from "./unicodeSymbols";
import utils from "./utils";
import {checkNodeType} from "./parseNode";
import ParseError from "./ParseError";
import {combiningDiacriticalMarksEndRegex} from "./Lexer";
import Settings from "./Settings";
import SourceLocation from "./SourceLocation";
import {Token} from "./Token";
import type {ParseNode, AnyParseNode, SymbolParseNode} from "./parseNode";
import type {ParseNode, AnyParseNode, SymbolParseNode, UnsupportedCmdParseNode}
from "./parseNode";
import type {Atom, Group} from "./symbols";
import type {Mode, ArgType, BreakToken} from "./types";
import type {FunctionContext, FunctionSpec} from "./defineFunction";
@@ -266,8 +266,7 @@ export default class Parser {
* Converts the textual input of an unsupported command into a text node
* contained within a color node whose color is determined by errorColor
*/
handleUnsupportedCmd(): AnyParseNode {
const text = this.nextToken.text;
formatUnsupportedCmd(text: string): UnsupportedCmdParseNode {
const textordArray = [];
for (let i = 0; i < text.length; i++) {
@@ -287,7 +286,6 @@ export default class Parser {
body: [textNode],
};
this.consume();
return colorNode;
}
@@ -723,14 +721,6 @@ export default class Parser {
// "undefined" behaviour, and keep them as-is. Some browser will
// replace backslashes with forward slashes.
const url = res.text.replace(/\\([#$%&~_^{}])/g, '$1');
let protocol = /^\s*([^\\/#]*?)(?::|&#0*58|&#x0*3a)/i.exec(url);
protocol = (protocol != null ? protocol[1] : "_relative");
const allowed = this.settings.allowedProtocols;
if (!utils.contains(allowed, "*") &&
!utils.contains(allowed, protocol)) {
throw new ParseError(
`Forbidden protocol '${protocol}'`, res);
}
return {
type: "url",
mode: this.mode,
@@ -803,7 +793,8 @@ export default class Parser {
throw new ParseError(
"Undefined control sequence: " + text, firstToken);
}
result = this.handleUnsupportedCmd();
result = this.formatUnsupportedCmd(text);
this.consume();
}
}

View File

@@ -16,6 +16,26 @@ export type StrictFunction =
(errorCode: string, errorMsg: string, token?: Token | AnyParseNode) =>
?(boolean | string);
export type TrustContextTypes = {
"\\href": {|
command: "\\href",
url: string,
protocol?: string,
|},
"\\includegraphics": {|
command: "\\includegraphics",
url: string,
protocol?: string,
|},
"\\url": {|
command: "\\url",
url: string,
protocol?: string,
|},
};
export type AnyTrustContext = $Values<TrustContextTypes>;
export type TrustFunction = (context: AnyTrustContext) => ?boolean;
export type SettingsOptions = {
displayMode?: boolean;
output?: "html" | "mathml" | "htmlAndMathml";
@@ -27,9 +47,9 @@ export type SettingsOptions = {
minRuleThickness?: number;
colorIsTextColor?: boolean;
strict?: boolean | "ignore" | "warn" | "error" | StrictFunction;
trust?: boolean | TrustFunction;
maxSize?: number;
maxExpand?: number;
allowedProtocols?: string[];
};
/**
@@ -42,7 +62,7 @@ export type SettingsOptions = {
* math (true), meaning that the math starts in \displaystyle
* and is placed in a block with vertical margin.
*/
class Settings {
export default class Settings {
displayMode: boolean;
output: "html" | "mathml" | "htmlAndMathml";
leqno: boolean;
@@ -53,9 +73,9 @@ class Settings {
minRuleThickness: number;
colorIsTextColor: boolean;
strict: boolean | "ignore" | "warn" | "error" | StrictFunction;
trust: boolean | TrustFunction;
maxSize: number;
maxExpand: number;
allowedProtocols: string[];
constructor(options: SettingsOptions) {
// allow null options
@@ -73,10 +93,9 @@ class Settings {
);
this.colorIsTextColor = utils.deflt(options.colorIsTextColor, false);
this.strict = utils.deflt(options.strict, "warn");
this.trust = utils.deflt(options.trust, false);
this.maxSize = Math.max(0, utils.deflt(options.maxSize, Infinity));
this.maxExpand = Math.max(0, utils.deflt(options.maxExpand, 1000));
this.allowedProtocols = utils.deflt(options.allowedProtocols,
["http", "https", "mailto", "_relative"]);
}
/**
@@ -146,6 +165,22 @@ class Settings {
return false;
}
}
}
export default Settings;
/**
* Check whether to test potentially dangerous input, and return
* `true` (trusted) or `false` (untrusted). The sole argument `context`
* should be an object with `command` field specifying the relevant LaTeX
* command (as a string starting with `\`), and any other arguments, etc.
* If `context` has a `url` field, a `protocol` field will automatically
* get added by this function (changing the specified object).
*/
isTrusted(context: AnyTrustContext) {
if (context.url && !context.protocol) {
context.protocol = utils.protocolFromUrl(context.url);
}
const trust = typeof this.trust === "function"
? this.trust(context)
: this.trust;
return Boolean(trust);
}
}

View File

@@ -2,7 +2,8 @@
import {checkNodeType} from "./parseNode";
import type Parser from "./Parser";
import type {ParseNode, AnyParseNode, NodeType} from "./parseNode";
import type {ParseNode, AnyParseNode, NodeType, UnsupportedCmdParseNode}
from "./parseNode";
import type Options from "./Options";
import type {ArgType, BreakToken, Mode} from "./types";
import type {HtmlDomNode} from "./domTree";
@@ -21,7 +22,9 @@ export type FunctionHandler<NODETYPE: NodeType> = (
context: FunctionContext,
args: AnyParseNode[],
optArgs: (?AnyParseNode)[],
) => ParseNode<NODETYPE>;
) => UnsupportedCmdParseNode | ParseNode<NODETYPE>;
// Note: reverse the order of the return type union will cause a flow error.
// See https://github.com/facebook/flow/issues/3663.
export type HtmlBuilder<NODETYPE> = (ParseNode<NODETYPE>, Options) => HtmlDomNode;
export type MathMLBuilder<NODETYPE> = (
@@ -199,10 +202,6 @@ export default function defineFunction<NODETYPE: NodeType>({
handler: handler,
};
for (let i = 0; i < names.length; ++i) {
// TODO: The value type of _functions should be a type union of all
// possible `FunctionSpec<>` possibilities instead of `FunctionSpec<*>`,
// which is an existential type.
// $FlowFixMe
_functions[names[i]] = data;
}
if (type) {

View File

@@ -18,6 +18,14 @@ defineFunction({
handler: ({parser}, args) => {
const body = args[1];
const href = assertNodeType(args[0], "url").url;
if (!parser.settings.isTrusted({
command: "\\href",
url: href,
})) {
return parser.formatUnsupportedCmd("\\href");
}
return {
type: "href",
mode: parser.mode,
@@ -49,6 +57,14 @@ defineFunction({
},
handler: ({parser}, args) => {
const href = assertNodeType(args[0], "url").url;
if (!parser.settings.isTrusted({
command: "\\url",
url: href,
})) {
return parser.formatUnsupportedCmd("\\url");
}
const chars = [];
for (let i = 0; i < href.length; i++) {
let c = href[i];

View File

@@ -85,6 +85,13 @@ defineFunction({
alt = alt.substring(0, alt.lastIndexOf('.'));
}
if (!parser.settings.isTrusted({
command: "\\includegraphics",
url: src,
})) {
return parser.formatUnsupportedCmd("\\includegraphics");
}
return {
type: "includegraphics",
mode: parser.mode,

View File

@@ -19,6 +19,9 @@ export type SymbolParseNode =
ParseNode<"spacing"> |
ParseNode<"textord">;
// ParseNode from `Parser.formatUnsupportedCmd`
export type UnsupportedCmdParseNode = ParseNode<"color">;
// Union of all possible `ParseNode<>` types.
export type AnyParseNode = $Values<ParseNodeTypes>;

View File

@@ -91,6 +91,15 @@ export const assert = function<T>(value: ?T): T {
return value;
};
/**
* Return the protocol of a URL, or "_relative" if the URL does not specify a
* protocol (and thus is relative).
*/
export const protocolFromUrl = function(url: string): string {
const protocol = /^\s*([^\\/#]*?)(?::|&#0*58|&#x0*3a)/i.exec(url);
return (protocol != null ? protocol[1] : "_relative");
};
export default {
contains,
deflt,
@@ -98,4 +107,5 @@ export default {
hyphenate,
getBaseElem,
isCharacterBox,
protocolFromUrl,
};