Repository: KaTeX/KaTeX (js)

Issue:
Security setting
We have several features/features in waiting that should probably only be used when the LaTeX code is from a trusted source:
* embedding arbitrary images (#1620, merged) could be used for tracking users 
* arbitrary `class/id` attributes (#1437) could break pages with duplicate `id`s in particular, allow unintended formatting (?)
* arbitrary HTML (#1596) could be a big security hole

I think we need some kind of `security` or `trust` option (like we did with `strict`) before these should be released. The simplest thing would be to have a Boolean `trusted` flag that turns on all of these features. Might we want more granularity?  Maybe `trusted: ['images', 'class']` turns on images + class but not ID and HTML?

Links and embedded images are quite different, so our existing URL restrictions don't suffice. On the other hand, we might want to replace our URL restrictions with this security/trust mechanism...?  For example, we might want to allow images from `http:` but not `file:`, like we can do for links.

Pull request description:
[breaking] trust setting to indicate whether input text is trusted
Here is a draft of a `trust` setting to indicate whether the input text is trusted (and thus fix #1771), also allowing a function (like we do with `strict`) to depend on the specific function that we're worried about (advanced usage).

I did not add support for an array of trusted things like I suggested in #1771, in order to keep this simple, but happy to add something like that if we want.

I also have not added any tests; wanted to see if people like this interface first.

Patch:
diff --git a/docs/options.md b/docs/options.md
index 70e9dec6..825bac9b 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -18,8 +18,8 @@ You can provide an object of options as the last argument to [`katex.render` and
 - `colorIsTextColor`: `boolean`. If `true`, `\color` will work like LaTeX's `\textcolor`, and take two arguments (e.g., `\color{blue}{hello}`), which restores the old behavior of KaTeX (pre-0.8.0). If `false` (the default), `\color` will work like LaTeX's `\color`, and take one argument (e.g., `\color{blue}hello`).  In both cases, `\textcolor` works as in LaTeX (e.g., `\textcolor{blue}{hello}`).
 - `maxSize`: `number`. All user-specified sizes, e.g. in `\rule{500em}{500em}`, will be capped to `maxSize` ems. If set to `Infinity` (the default), users can make elements and spaces arbitrarily large.
 - `maxExpand`: `number`. Limit the number of macro expansions to the specified number, to prevent e.g. infinite macro loops. If set to `Infinity`, the macro expander will try to fully expand as in LaTeX. (default: 1000)
-- `allowedProtocols`: `string[]`. Allowed protocols in `\href`. Use `_relative` to allow relative urls, and `*` to allow all protocols. (default: `["http", "https", "mailto", "_relative"]`)
 - `strict`: `boolean` or `string` or `function` (default: `"warn"`). If `false` or `"ignore`", allow features that make writing LaTeX convenient but are not actually supported by (Xe)LaTeX (similar to MathJax). If `true` or `"error"` (LaTeX faithfulness mode), throw an error for any such transgressions. If `"warn"` (the default), warn about such behavior via `console.warn`. Provide a custom function `handler(errorCode, errorMsg, token)` to customize behavior depending on the type of transgression (summarized by the string code `errorCode` and detailed in `errorMsg`); this function can also return `"ignore"`, `"error"`, or `"warn"` to use a built-in behavior.  A list of such features and their `errorCode`s:
+
   - `"unknownSymbol"`: Use of unknown Unicode symbol, which will likely also
     lead to warnings about missing character metrics, and layouts may be
     incorrect (especially in terms of vertical heights).
@@ -31,10 +31,27 @@ You can provide an object of options as the last argument to [`katex.render` and
 
   A second category of `errorCode`s never throw errors, but their strictness
   affects the behavior of KaTeX:
+
   - `"newLineInDisplayMode"`: Use of `\\` or `\newline` in display mode
     (outside an array/tabular environment).  In strict mode, no line break
     results, as in LaTeX.
 
+- `trust`: `boolean` or `function` (default: `false`). If `false` (do not trust input), prevent any commands like `\includegraphics` that could enable adverse behavior, rendering them instead in `errorColor`. If `true` (trust input), allow all such commands. Provide a custom function `handler(context)` to customize behavior depending on the context (command, arguments e.g. a URL, etc.).  A list of possible contexts:
+
+  - `{command: "\\url", url, protocol}`
+  - `{command: "\\href", url, protocol}`
+  - `{command: "\\includegraphics", url, protocol}`
+
+  Here are some sample trust settings:
+
+  - Forbid specific command: `trust: (context) => context.command !== '\\includegraphics'`
+  - Allow specific command: `trust: (context) => context.command === '\\url'`
+  - Allow multiple specific commands: `trust: (context) => ['\\url', '\\href'].includes(context.command)`
+  - Allow all commands with a specific protocol: `trust: (context) => context.protocol === 'http'`
+  - Allow all commands with specific protocols: `trust: (context) => ['http', 'https', '_relative'].includes(context.protocol)`
+  - Allow all commands but forbid specific protocol: `trust: (context) => context.protocol !== 'file'`
+  - Allow certain commands with specific protocols: `trust: (context) => ['\\url', '\\href'].includes(context.command) && ['http', 'https', '_relative'].includes(context.protocol)`
+
 For example:
 
 ```js
diff --git a/docs/security.md b/docs/security.md
index 1f9b0614..609d0b05 100644
--- a/docs/security.md
+++ b/docs/security.md
@@ -9,10 +9,11 @@ Of course, it is always a good idea to sanitize the HTML, though you will need
 a rather generous whitelist (including some of SVG and MathML) to support
 all of KaTeX.
 
-Use `maxSize` option for preventing large width/height visual affronts,
-use `maxExpand` for preventing infinite macro loop attacks, and
-use `allowedProtocols` for preventing certain protocols in `\href`. Please
-refer to [Options](options.md) for more details.
+A variety of options give finer control over the security of KaTeX
+with untrusted inputs; refer to [Options](options.md) for more details.
+* `maxSize` can prevent large width/height visual affronts.
+* `maxExpand` can prevent infinite macro loop attacks.
+* `trust` can allow certain commands that are not always safe (e.g., `\includegraphics`)
 
 The error message thrown by KaTeX may contain unescaped LaTeX source code.
 See [Handling Errors](error.md) for more details.
diff --git a/docs/support_table.md b/docs/support_table.md
index 8b377a43..4343982f 100644
--- a/docs/support_table.md
+++ b/docs/support_table.md
@@ -453,7 +453,7 @@ use `\ce` instead|
 |\hookleftarrow|$\hookleftarrow$||
 |\hookrightarrow|$\hookrightarrow$||
 |\hphantom|$a\hphantom{bc}d$|`a\hphantom{bc}d`|
-|\href|$\href{https://katex.org/}{\KaTeX}$|`\href{https://katex.org/}{\KaTeX}`|
+|\href|$\href{https://katex.org/}{\KaTeX}$|`\href{https://katex.org/}{\KaTeX}` Requires `trust` [option](options.md)|
 |\hskip|$w\hskip1em i\hskip2em d$|`w\hskip1em i\hskip2em d`|
 |\hslash|$\hslash$||
 |\hspace|$s\hspace7ex k$|`s\hspace7ex k`|
@@ -1105,7 +1105,7 @@ use `\ce` instead|
 |\upsilon|$\upsilon$||
 |\upuparrows|$\upuparrows$||
 |\urcorner|$\urcorner$||
-|\url|$\footnotesize\url{https://katex.org/}$|`\url{https://katex.org/}`|
+|\url|$\footnotesize\url{https://katex.org/}$|`\url{https://katex.org/}` Requires `trust` [option](options.md)|
 |\utilde|$\utilde{AB}$|`\utilde{AB}`|
 
 ## V
diff --git a/docs/supported.md b/docs/supported.md
index 2662618f..e770f74a 100644
--- a/docs/supported.md
+++ b/docs/supported.md
@@ -101,6 +101,13 @@ The `{array}` environment does not yet support `\cline` or `\multicolumn`.
 
 ## HTML
 
+The following "raw HTML" features are potentially dangerous for untrusted
+inputs, so they are disabled by default, and attempting to use them produces
+the command names in red (which you can configure via the `errorColor`
+[option](options.md)).  To fully trust your LaTeX input, you need to pass
+an option of `trust: true`; you can also enable just some of the commands
+or for just some URLs via the `trust` [option](options.md).
+
 |||
 |:----------------|:-------------------|
 | $\href{https://katex.org/}{\KaTeX}$ | `\href{https://katex.org/}{\KaTeX}` |
diff --git a/src/Parser.js b/src/Parser.js
index b880b1d8..fa2c39ac 100644
--- a/src/Parser.js
+++ b/src/Parser.js
@@ -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();
             }
         }
 
diff --git a/src/Settings.js b/src/Settings.js
index 59be4f22..78755181 100644
--- a/src/Settings.js
+++ b/src/Settings.js
@@ -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);
+    }
+}
diff --git a/src/defineFunction.js b/src/defineFunction.js
index 58fb4985..f866b665 100644
--- a/src/defineFunction.js
+++ b/src/defineFunction.js
@@ -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) {
diff --git a/src/functions/href.js b/src/functions/href.js
index 17001fb4..d2cd2df6 100644
--- a/src/functions/href.js
+++ b/src/functions/href.js
@@ -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];
diff --git a/src/functions/includegraphics.js b/src/functions/includegraphics.js
index 70cb427b..aaa7e505 100644
--- a/src/functions/includegraphics.js
+++ b/src/functions/includegraphics.js
@@ -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,
diff --git a/src/parseNode.js b/src/parseNode.js
index 1fe2a6f4..f8e8ec0a 100644
--- a/src/parseNode.js
+++ b/src/parseNode.js
@@ -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>;
 
diff --git a/src/utils.js b/src/utils.js
index b364ad8d..534c0f58 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -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,
 };
diff --git a/website/lib/remarkable-katex.js b/website/lib/remarkable-katex.js
index b2e35766..afd50bd2 100644
--- a/website/lib/remarkable-katex.js
+++ b/website/lib/remarkable-katex.js
@@ -29,7 +29,8 @@ module.exports = function(md, options) {
     const katex = require("../../");
 
     function renderKatex(source, displayMode) {
-        return katex.renderToString(source, {displayMode, throwOnError: false});
+        return katex.renderToString(source,
+            {displayMode, throwOnError: false, trust: true});
     }
 
     /**


Test patch:
diff --git a/test/__snapshots__/katex-spec.js.snap b/test/__snapshots__/katex-spec.js.snap
index ffd69040..4ff695e9 100755
--- a/test/__snapshots__/katex-spec.js.snap
+++ b/test/__snapshots__/katex-spec.js.snap
@@ -970,6 +970,151 @@ exports[`Newlines via \\\\ and \\newline \\\\ causes newline, even after mrel an
 
 `;
 
+exports[`href and url commands should allow all protocols when trust option is true 1`] = `
+[
+  {
+    "type": "href",
+    "body": [
+      {
+        "type": "mathord",
+        "loc": {
+          "end": 16,
+          "lexer": {
+            "input": "\\\\href{ftp://x}{foo}",
+            "lastIndex": 19
+          },
+          "start": 15
+        },
+        "mode": "math",
+        "text": "f"
+      },
+      {
+        "type": "mathord",
+        "loc": {
+          "end": 17,
+          "lexer": {
+            "input": "\\\\href{ftp://x}{foo}",
+            "lastIndex": 19
+          },
+          "start": 16
+        },
+        "mode": "math",
+        "text": "o"
+      },
+      {
+        "type": "mathord",
+        "loc": {
+          "end": 18,
+          "lexer": {
+            "input": "\\\\href{ftp://x}{foo}",
+            "lastIndex": 19
+          },
+          "start": 17
+        },
+        "mode": "math",
+        "text": "o"
+      }
+    ],
+    "href": "ftp://x",
+    "mode": "math"
+  }
+]
+`;
+
+exports[`href and url commands should allow explicitly allowed protocols 1`] = `
+[
+  {
+    "type": "href",
+    "body": [
+      {
+        "type": "mathord",
+        "loc": {
+          "end": 16,
+          "lexer": {
+            "input": "\\\\href{ftp://x}{foo}",
+            "lastIndex": 19
+          },
+          "start": 15
+        },
+        "mode": "math",
+        "text": "f"
+      },
+      {
+        "type": "mathord",
+        "loc": {
+          "end": 17,
+          "lexer": {
+            "input": "\\\\href{ftp://x}{foo}",
+            "lastIndex": 19
+          },
+          "start": 16
+        },
+        "mode": "math",
+        "text": "o"
+      },
+      {
+        "type": "mathord",
+        "loc": {
+          "end": 18,
+          "lexer": {
+            "input": "\\\\href{ftp://x}{foo}",
+            "lastIndex": 19
+          },
+          "start": 17
+        },
+        "mode": "math",
+        "text": "o"
+      }
+    ],
+    "href": "ftp://x",
+    "mode": "math"
+  }
+]
+`;
+
+exports[`href and url commands should forbid relative URLs when trust option is false 1`] = `
+[
+  {
+    "type": "color",
+    "body": [
+      {
+        "type": "text",
+        "body": [
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "\\\\"
+          },
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "h"
+          },
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "r"
+          },
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "e"
+          },
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "f"
+          }
+        ],
+        "mode": "math"
+      }
+    ],
+    "color": "#cc0000",
+    "mode": "math"
+  }
+]
+`;
+
 exports[`href and url commands should not affect spacing around 1`] = `
 [
   {
@@ -1062,3 +1207,46 @@ exports[`href and url commands should not affect spacing around 1`] = `
   }
 ]
 `;
+
+exports[`href and url commands should not allow explicitly disallow protocols 1`] = `
+[
+  {
+    "type": "color",
+    "body": [
+      {
+        "type": "text",
+        "body": [
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "\\\\"
+          },
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "h"
+          },
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "r"
+          },
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "e"
+          },
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "f"
+          }
+        ],
+        "mode": "math"
+      }
+    ],
+    "color": "#cc0000",
+    "mode": "math"
+  }
+]
+`;
diff --git a/test/katex-spec.js b/test/katex-spec.js
index 95e03fac..050d52b8 100644
--- a/test/katex-spec.js
+++ b/test/katex-spec.js
@@ -2682,17 +2682,17 @@ describe("href and url commands", function() {
 
     it("should allow letters [#$%&~_^] without escaping", function() {
         const url = "http://example.org/~bar/#top?foo=$foo&bar=ba^r_boo%20baz";
-        const parsed1 = getParsed(`\\href{${url}}{\\alpha}`)[0];
+        const parsed1 = getParsed(`\\href{${url}}{\\alpha}`, new Settings({trust: true}))[0];
         expect(parsed1.href).toBe(url);
-        const parsed2 = getParsed(`\\url{${url}}`)[0];
+        const parsed2 = getParsed(`\\url{${url}}`, new Settings({trust: true}))[0];
         expect(parsed2.href).toBe(url);
     });
 
     it("should allow balanced braces in url", function() {
         const url = "http://example.org/{{}t{oo}}";
-        const parsed1 = getParsed(`\\href{${url}}{\\alpha}`)[0];
+        const parsed1 = getParsed(`\\href{${url}}{\\alpha}`, new Settings({trust: true}))[0];
         expect(parsed1.href).toBe(url);
-        const parsed2 = getParsed(`\\url{${url}}`)[0];
+        const parsed2 = getParsed(`\\url{${url}}`, new Settings({trust: true}))[0];
         expect(parsed2.href).toBe(url);
     });
 
@@ -2706,9 +2706,9 @@ describe("href and url commands", function() {
     it("should allow escape for letters [#$%&~_^{}]", function() {
         const url = "http://example.org/~bar/#top?foo=$}foo{&bar=bar^r_boo%20baz";
         const input = url.replace(/([#$%&~_^{}])/g, '\\$1');
-        const parsed1 = getParsed(`\\href{${input}}{\\alpha}`)[0];
+        const parsed1 = getParsed(`\\href{${input}}{\\alpha}`, new Settings({trust: true}))[0];
         expect(parsed1.href).toBe(url);
-        const parsed2 = getParsed(`\\url{${input}}`)[0];
+        const parsed2 = getParsed(`\\url{${input}}`, new Settings({trust: true}))[0];
         expect(parsed2.href).toBe(url);
     });
 
@@ -2717,30 +2717,42 @@ describe("href and url commands", function() {
     });
 
     it("should be marked up correctly", function() {
-        const markup = katex.renderToString(r`\href{http://example.com/}{example here}`);
+        const markup = katex.renderToString(r`\href{http://example.com/}{example here}`, {trust: true});
         expect(markup).toContain("<a href=\"http://example.com/\">");
     });
 
-    it("should allow protocols in allowedProtocols", function() {
-        expect("\\href{relative}{foo}").toParse();
-        expect("\\href{ftp://x}{foo}").toParse(new Settings({
-            allowedProtocols: ["ftp"],
-        }));
-        expect("\\href{ftp://x}{foo}").toParse(new Settings({
-            allowedProtocols: ["*"],
-        }));
+    it("should not affect spacing around", function() {
+        const built = getBuilt("a\\href{http://example.com/}{+b}", new Settings({trust: true}));
+        expect(built).toMatchSnapshot();
     });
 
-    it("should not allow protocols not in allowedProtocols", function() {
-        expect("\\href{javascript:alert('x')}{foo}").not.toParse();
-        expect("\\href{relative}{foo}").not.toParse(new Settings({
-            allowedProtocols: [],
-        }));
+    it("should forbid relative URLs when trust option is false", () => {
+        const parsed = getParsed("\\href{relative}{foo}");
+        expect(parsed).toMatchSnapshot();
     });
 
-    it("should not affect spacing around", function() {
-        const built = getBuilt`a\href{http://example.com/}{+b}`;
-        expect(built).toMatchSnapshot();
+    it("should allow explicitly allowed protocols", () => {
+        const parsed = getParsed(
+            "\\href{ftp://x}{foo}",
+            new Settings({trust: (context) => context.protocol === "ftp"}),
+        );
+        expect(parsed).toMatchSnapshot();
+    });
+
+    it("should allow all protocols when trust option is true", () => {
+        const parsed = getParsed(
+            "\\href{ftp://x}{foo}",
+            new Settings({trust: true}),
+        );
+        expect(parsed).toMatchSnapshot();
+    });
+
+    it("should not allow explicitly disallow protocols", () => {
+        const parsed = getParsed(
+            "\\href{javascript:alert('x')}{foo}",
+            new Settings({trust: context => context.protocol !== "javascript"}),
+        );
+        expect(parsed).toMatchSnapshot();
     });
 });
 
diff --git a/test/mathml-spec.js b/test/mathml-spec.js
index b44db6b4..5a1314ac 100644
--- a/test/mathml-spec.js
+++ b/test/mathml-spec.js
@@ -73,7 +73,9 @@ describe("A MathML builder", function() {
     });
 
     it('should set href attribute for href appropriately', () => {
-        expect(getMathML("\\href{http://example.org}{\\alpha}")).toMatchSnapshot();
+        expect(
+            getMathML("\\href{http://example.org}{\\alpha}", new Settings({trust: true})),
+        ).toMatchSnapshot();
         expect(getMathML("p \\Vdash \\beta \\href{http://example.org}{+ \\alpha} \\times \\gamma"));
     });
 
diff --git a/test/screenshotter/test.html b/test/screenshotter/test.html
index 5ba4827c..6809558b 100644
--- a/test/screenshotter/test.html
+++ b/test/screenshotter/test.html
@@ -70,7 +70,8 @@
 
         var settings = {
             displayMode: !!query["display"],
-            throwOnError: !query["noThrow"]
+            throwOnError: !query["noThrow"],
+            trust: true  // trust test inputs
         };
         if (query["errorColor"]) {
             settings.errorColor = query["errorColor"];