Skip to content

Commit

Permalink
Delegate trust check to action handler (ampproject#21553)
Browse files Browse the repository at this point in the history
* Delegate trust check to action handler.

* @note is not a valid JSDoc tag name?
  • Loading branch information
William Chou committed Mar 23, 2019
1 parent 8af5044 commit 391a431
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 45 deletions.
4 changes: 4 additions & 0 deletions extensions/amp-access/0.1/amp-access.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import {AccessSource, AccessType} from './amp-access-source';
import {AccessVars} from './access-vars';
import {ActionTrust} from '../../../src/action-constants';
import {AmpEvents} from '../../../src/amp-events';
import {CSS} from '../../../build/amp-access-0.1.css';
import {Observable} from '../../../src/observable';
Expand Down Expand Up @@ -648,6 +649,9 @@ export class AccessService {
* @private
*/
handleAction_(invocation) {
if (!invocation.satisfiesTrust(ActionTrust.HIGH)) {
return;
}
if (invocation.method == 'login') {
if (invocation.event) {
invocation.event.preventDefault();
Expand Down
5 changes: 4 additions & 1 deletion extensions/amp-form/0.1/amp-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export class AmpForm {
this.form_, () => this.handleXhrVerify_());

this.actions_.installActionHandler(
this.form_, this.actionHandler_.bind(this), ActionTrust.HIGH);
this.form_, this.actionHandler_.bind(this));
this.installEventHandlers_();
this.installInputMasking_();

Expand Down Expand Up @@ -313,6 +313,9 @@ export class AmpForm {
* @private
*/
actionHandler_(invocation) {
if (!invocation.satisfiesTrust(ActionTrust.HIGH)) {
return null;
}
if (invocation.method == 'submit') {
return this.whenDependenciesReady_().then(() => {
return this.handleSubmitAction_(invocation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ export class AmpViewerAssistance {
return this;
}
this.action_.installActionHandler(
this.assistanceElement_, this.actionHandler_.bind(this),
ActionTrust.HIGH);
this.assistanceElement_, this.actionHandler_.bind(this));

this.getIdTokenPromise();

Expand Down
70 changes: 37 additions & 33 deletions src/service/action-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const DEFAULT_DEBOUNCE_WAIT = 300; // ms
const DEFAULT_THROTTLE_INTERVAL = 100; // ms

/** @const {!Object<string,!Array<string>>} */
const ELEMENTS_ACTIONS_MAP_ = {
const NON_AMP_ELEMENTS_ACTIONS_ = {
'form': ['submit', 'clear'],
};

Expand Down Expand Up @@ -254,6 +254,7 @@ export class ActionService {
* @const @private {!Object<string, {handler: ActionHandlerDef, minTrust: ActionTrust}>}
*/
this.globalMethodHandlers_ = map();

// Add core events.
this.addEvent('tap');
this.addEvent('submit');
Expand Down Expand Up @@ -393,39 +394,37 @@ export class ActionService {
}

/**
* Installs action handler for the specified element.
* Installs action handler for the specified element. The action handler is
* responsible for checking invocation trust.
*
* For AMP elements, use base-element.registerAction() instead.
*
* @param {!Element} target
* @param {ActionHandlerDef} handler
* @param {ActionTrust} minTrust
*/
installActionHandler(target, handler, minTrust = ActionTrust.HIGH) {
installActionHandler(target, handler) {
// TODO(dvoytenko, #7063): switch back to `target.id` with form proxy.
const targetId = target.getAttribute('id') || '';
const debugId = target.tagName + '#' + targetId;
devAssert((targetId && targetId.substring(0, 4) == 'amp-') ||
target.tagName.toLowerCase() in ELEMENTS_ACTIONS_MAP_,
'AMP element or a whitelisted target element is expected: %s', debugId);

devAssert(isAmpTagName(targetId) ||
target.tagName.toLowerCase() in NON_AMP_ELEMENTS_ACTIONS_,
'AMP or special element expected: %s', target.tagName + '#' + targetId);

if (target[ACTION_HANDLER_]) {
dev().error(TAG_, `Action handler already installed for ${target}`);
return;
}
target[ACTION_HANDLER_] = handler;

/** @const {Array<!ActionInvocation>} */
const currentQueue = target[ACTION_QUEUE_];

target[ACTION_HANDLER_] = {handler, minTrust};

// Dequeue the current queue.
if (isArray(currentQueue)) {
const queuedInvocations = target[ACTION_QUEUE_];
if (isArray(queuedInvocations)) {
// Invoke and clear all queued invocations now handler is installed.
Services.timerFor(toWin(target.ownerDocument.defaultView)).delay(() => {
// TODO(dvoytenko, #1260): dedupe actions.
currentQueue.forEach(invocation => {
queuedInvocations.forEach(invocation => {
try {
if (invocation.satisfiesTrust(
/** @type {ActionTrust} */ (minTrust))) {
handler(invocation);
}
handler(invocation);
} catch (e) {
dev().error(TAG_, 'Action execution failed:', invocation, e);
}
Expand Down Expand Up @@ -552,7 +551,6 @@ export class ActionService {
}
}


/**
* @param {!ActionInvocation} invocation
* @return {?Promise}
Expand All @@ -563,7 +561,7 @@ export class ActionService {

// Check that this action is whitelisted (if a whitelist is set).
if (this.whitelist_) {
if (!isActionWhitelisted_(invocation, this.whitelist_)) {
if (!isActionWhitelisted(invocation, this.whitelist_)) {
this.error_(`"${tagOrTarget}.${method}" is not whitelisted ${
JSON.stringify(this.whitelist_)}.`);
return null;
Expand All @@ -587,7 +585,7 @@ export class ActionService {

// Handle element-specific actions.
const lowerTagName = node.tagName.toLowerCase();
if (lowerTagName.substring(0, 4) == 'amp-') {
if (isAmpTagName(lowerTagName)) {
if (node.enqueAction) {
node.enqueAction(invocation);
} else {
Expand All @@ -596,18 +594,15 @@ export class ActionService {
return null;
}

// Special elements with AMP ID or known supported actions.
const supportedActions = ELEMENTS_ACTIONS_MAP_[lowerTagName];
// Special non-AMP elements with AMP ID or known supported actions.
const nonAmpActions = NON_AMP_ELEMENTS_ACTIONS_[lowerTagName];
// TODO(dvoytenko, #7063): switch back to `target.id` with form proxy.
const targetId = node.getAttribute('id') || '';
if ((targetId && targetId.substring(0, 4) == 'amp-') ||
(supportedActions && supportedActions.indexOf(method) > -1)) {
const holder = node[ACTION_HANDLER_];
if (holder) {
const {handler, minTrust} = holder;
if (invocation.satisfiesTrust(minTrust)) {
handler(invocation);
}
if (isAmpTagName(targetId) ||
(nonAmpActions && nonAmpActions.indexOf(method) > -1)) {
const handler = node[ACTION_HANDLER_];
if (handler) {
handler(invocation);
} else {
node[ACTION_QUEUE_] = node[ACTION_QUEUE_] || [];
node[ACTION_QUEUE_].push(invocation);
Expand Down Expand Up @@ -766,6 +761,15 @@ export class ActionService {
}
}

/**
* @param {string} lowercaseTagName
* @return {boolean}
* @private
*/
function isAmpTagName(lowercaseTagName) {
return lowercaseTagName.substring(0, 4) === 'amp-';
}

/**
* Returns `true` if the given action invocation is whitelisted in the given
* whitelist. Default actions' alias, 'activate', are automatically
Expand All @@ -775,7 +779,7 @@ export class ActionService {
* @return {boolean}
* @private
*/
function isActionWhitelisted_(invocation, whitelist) {
function isActionWhitelisted(invocation, whitelist) {
let {method} = invocation;
const {node, tagOrTarget} = invocation;
// Use alias if default action is invoked.
Expand Down
17 changes: 8 additions & 9 deletions test/unit/test-action.js
Original file line number Diff line number Diff line change
Expand Up @@ -869,20 +869,19 @@ describe('installActionHandler', () => {
expect(callArgs.trust).to.be.equal(ActionTrust.HIGH);
});

it('should check trust level before invoking action', () => {
it('should not check trust level (handler should check)', () => {
const handlerSpy = sandbox.spy();
const target = document.createElement('form');
action.installActionHandler(target, handlerSpy, ActionTrust.HIGH);
action.installActionHandler(target, handlerSpy);

action.invoke_(new ActionInvocation(target, 'submit', /* args */ null,
'button', 'button', 'tapEvent', ActionTrust.HIGH));
const invocation = new ActionInvocation(target, 'submit', /* args */ null,
'button', 'button', 'tapEvent', ActionTrust.HIGH);
action.invoke_(invocation);
expect(handlerSpy).to.be.calledOnce;

return allowConsoleError(() => {
action.invoke_(new ActionInvocation(target, 'submit', /* args */ null,
'button', 'button', 'tapEvent', ActionTrust.LOW));
expect(handlerSpy).to.be.calledOnce;
});
invocation.trust = ActionTrust.LOW;
action.invoke_(invocation);
expect(handlerSpy).to.be.calledTwice;
});
});

Expand Down

0 comments on commit 391a431

Please sign in to comment.