Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

amp-mustache: Output user errors when elements/attributes are sanitized #20285

Merged
merged 4 commits into from
Jan 15, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just combine nodeName check in the initial element check.

if (r.attribute) {
   const {name, value} = r.attribute;
   user().error(TAG, `Removed unsafe attribute: ${name}="${value}"`);
} else if (r.element && r.element.nodeName !== 'REMOVE') {
...
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads more like prose to me:

// If an attribute is removed...
// Otherwise, if an element is removed...
//     If the element is <remove>...

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should we keep this comment and move it to where you declared TIMEOUT.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not referencing mochaTimeout anymore, so there's no need to refer to "default timeout".

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