Skip to content

Commit

Permalink
amp-mustache: Output user errors when elements/attributes are sanitiz…
Browse files Browse the repository at this point in the history
…ed (ampproject#20285)

* Output user errors for elements/attrs sanitized by DOMPurify.

* Make sanitizer error format consistent.

* Add documentation with sanitization caveat.

* Increase timeout of test-amp-bind to 15s.
  • Loading branch information
William Chou authored and Noran Azmy committed Mar 22, 2019
1 parent 38d845d commit 2e76530
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 8 deletions.
10 changes: 10 additions & 0 deletions extensions/amp-mustache/amp-mustache.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ target AMP element that uses this template to render its content (for example, i

## Restrictions

### Validation

Like all AMP templates, `amp-mustache` templates are required to be well-formed DOM fragments. This means
that among other things, you can't use `amp-mustache` to:

Expand All @@ -85,6 +87,14 @@ that among other things, you can't use `amp-mustache` to:

The output of "triple-mustache" is sanitized to only allow the following tags: `a`, `b`, `br`, `caption`, `colgroup`, `code`, `del`, `div`, `em`, `i`, `ins`, `li`, `mark`, `ol`, `p`, `q`, `s`, `small`, `span`, `strong`, `sub`, `sup`, `table`, `tbody`, `time`, `td`, `th`, `thead`, `tfoot`, `tr`, `u`, `ul`.

### Sanitization

Mustache output is sanitized for security reasons, which may result in certain elements and attributes being removed.

A console error will be output with details of the sanitized element or attribute. For example:

> [PURIFIER] Removed unsafe attribute: href="javascript:alert"
## Pitfalls

### Nested templates
Expand Down
18 changes: 15 additions & 3 deletions src/purifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ let DomPurifyDef;
const DomPurify = purify(self);

/** @private @const {string} */
const TAG = 'purifier';
const TAG = 'PURIFIER';

/** @private @const {string} */
const ORIGINAL_TARGET_VALUE = '__AMP_ORIGINAL_TARGET_VALUE_';
Expand Down Expand Up @@ -339,6 +339,7 @@ export function addPurifyHooks(purifier, diffing) {

/**
* @param {!Node} unusedNode
* @this {{removed: !Array}} Contains list of removed elements/attrs so far.
*/
const afterSanitizeElements = function(unusedNode) {
// DOMPurify doesn't have a attribute-specific tag whitelist API and
Expand All @@ -348,6 +349,19 @@ export function addPurifyHooks(purifier, diffing) {
delete allowedTags[tag];
});
allowedTagsChanges.length = 0;

// Output user errors for each removed attribute or element.
this.removed.forEach(r => {
if (r.attribute) {
const {name, value} = r.attribute;
user().error(TAG, `Removed unsafe attribute: ${name}="${value}"`);
} else if (r.element) {
const {nodeName} = r.element;
if (nodeName !== 'REMOVE') { // <remove> is added by DOMPurify.
user().error(TAG, 'Removed unsafe element:', r.element.nodeName);
}
}
});
};

/**
Expand Down Expand Up @@ -427,8 +441,6 @@ export function addPurifyHooks(purifier, diffing) {
attrValue = rewriteAttributeValue(tagName, attrName, attrValue);
}
} else {
user().error(TAG, `Removing "${attrName}" attribute with invalid `
+ `value in <${tagName} ${attrName}="${attrValue}">.`);
data.keepAttr = false;
}

Expand Down
6 changes: 3 additions & 3 deletions src/sanitizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {startsWith} from './string';
import {user} from './log';

/** @private @const {string} */
const TAG = 'sanitizer';
const TAG = 'SANITIZER';

/**
* Whitelist of supported self-closing tags for Caja. These are used for
Expand Down Expand Up @@ -206,8 +206,8 @@ export function sanitizeHtml(html, diffing) {
const attrName = attribs[i];
const attrValue = attribs[i + 1];
if (!isValidAttr(tagName, attrName, attrValue)) {
user().error(TAG, `Removing "${attrName}" attribute with invalid `
+ `value in <${tagName} ${attrName}="${attrValue}">.`);
user().error(TAG,
`Removed unsafe attribute: ${attrName}="${attrValue}"`);
continue;
}
emit(' ');
Expand Down
5 changes: 3 additions & 2 deletions test/integration/test-amp-bind.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ import {FormEvents} from '../../extensions/amp-form/0.1/form-events';
import {Services} from '../../src/services';
import {poll as classicPoll, createFixtureIframe} from '../../testing/iframe';

const TIMEOUT = 15000;

// Skip Edge, which throws "Permission denied" errors when inspecting
// element properties in the testing iframe (Edge 17, Windows 10).
describe.configure().skipEdge().run('amp-bind', function() {
// Give more than default 2000ms timeout for local testing.
const TIMEOUT = Math.max(window.ampTestRuntimeConfig.mochaTimeout, 4000);
this.timeout(TIMEOUT);

// Helper that sets the poll timeout.
function poll(desc, condition, onError) {
return classicPoll(desc, condition, onError, TIMEOUT);
}
Expand Down

0 comments on commit 2e76530

Please sign in to comment.