src/sanitizer.js
/**
 * Sanitizes the provided HTML.
 *
 * This function expects the HTML to be already pre-sanitized and thus it does
 * not validate all of the AMP rules - only the most dangerous security-related
 * cases, such as <SCRIPT>, <STYLE>, <IFRAME>.
 *
 * @param {string} html
 * @param {!Document} doc
 * @return {string}
 */
Sanitizes the provided HTML.
This function expects the HTML to be already pre-sanitized and thus it does
not validate all of the AMP rules - only the most dangerous security-related
cases, such as 

@param {string} html
@param {!Document} doc
@return {string}
Sanitizes the provided HTML . This function expects the HTML to be already pre - sanitized and thus it does not validate all of the AMP rules - only the most dangerous security - related cases such as @param { string } html @param { !Document } doc @return { string }
function sanitizeHtml(html, doc) {
  const tagPolicy = htmlSanitizer.makeTagPolicy((parsed) =>
    parsed.getScheme() === 'https' ? parsed : null
  );
  const output = [];
  let ignore = 0;
  const emit = (content) => {
    if (ignore == 0) {
      output.push(content);
    }
  };
  const cajaDenylistedTags = {
    'script': true,
    'svg': true,
    ...DENYLISTED_TAGS,
  };
  const parser = htmlSanitizer.makeSaxParser({
    'startTag': function (tagName, attribs) {
      if (ignore > 0) {
        if (!SELF_CLOSING_TAGS[tagName]) {
          ignore++;
        }
        return;
      }
      const isAmpElement = tagName.startsWith('amp-');
      const bindingAttribs = [];
      for (let i = 0; i < attribs.length; i += 2) {
        const attr = attribs[i];
        if (!attr) {
          continue;
        }
        const classicBinding = attr[0] == '[' && attr[attr.length - 1] == ']';
        const alternativeBinding = attr.startsWith(BIND_PREFIX);
        if (classicBinding) {
          attribs[i] = attr.slice(1, -1);
        }
        if (classicBinding || alternativeBinding) {
          bindingAttribs.push(i);
        }
      }
      if (cajaDenylistedTags[tagName]) {
        ignore++;
      } else if (isAmpElement) {
        if (isAmp4Email(doc) && !EMAIL_ALLOWLISTED_AMP_TAGS[tagName]) {
          ignore++;
        }
      } else {
        const savedAttribs = attribs.slice(0);
        const scrubbed =  (
          tagPolicy(tagName, attribs)
        );
        if (!scrubbed) {
          ignore++;
        } else {
          attribs = scrubbed['attribs'];
          for (let i = 0; i < attribs.length; i += 2) {
            const attrName = attribs[i];
            if (ALLOWLISTED_ATTRS.includes(attrName)) {
              attribs[i + 1] = savedAttribs[i + 1];
            } else if (attrName.search(ALLOWLISTED_ATTR_PREFIX_REGEX) == 0) {
              attribs[i + 1] = savedAttribs[i + 1];
            } else if (
              ALLOWLISTED_ATTRS_BY_TAGS[tagName] &&
              ALLOWLISTED_ATTRS_BY_TAGS[tagName].includes(attrName)
            ) {
              attribs[i + 1] = savedAttribs[i + 1];
            }
          }
        }
        if (tagName == 'a') {
          let index = -1;
          let hasHref = false;
          for (let i = 0; i < savedAttribs.length; i += 2) {
            if (savedAttribs[i] == 'target') {
              index = i + 1;
            } else if (savedAttribs[i] == 'href') {
              hasHref = attribs[i + 1] != null;
            }
          }
          let origTarget = index != -1 ? savedAttribs[index] : null;
          if (origTarget != null) {
            origTarget = origTarget.toLowerCase();
            if (ALLOWLISTED_TARGETS.indexOf(origTarget) != -1) {
              attribs[index] = origTarget;
            } else {
              attribs[index] = '_top';
            }
          } else if (hasHref) {
            attribs.push('target', '_top');
          }
        }
      }
      if (ignore > 0) {
        if (SELF_CLOSING_TAGS[tagName]) {
          ignore--;
        }
        return;
      }
      const hasBindings = bindingAttribs.some((i) => !!attribs[i + 1]);
      if (hasBindings) {
        attribs.push('i-amphtml-binding', '');
      }
      emit('<');
      emit(tagName);
      for (let i = 0; i < attribs.length; i += 2) {
        const attrName = attribs[i];
        const attrValue = attribs[i + 1];
        if (!isValidAttr(tagName, attrName, attrValue, doc, false)) {
          user().error(
            TAG,
            `Removing "${attrName}" attribute with invalid ` +
              `value in <${tagName} ${attrName}="${attrValue}">.`
          );
          continue;
        }
        emit(' ');
        if (bindingAttribs.includes(i) && !attrName.startsWith(BIND_PREFIX)) {
          emit(`[${attrName}]`);
        } else {
          emit(attrName);
        }
        emit('="');
        if (attrValue) {
          const rewrite = bindingAttribs.includes(i)
            ? attrValue
            : rewriteAttributeValue(tagName, attrName, attrValue);
          emit(htmlSanitizer.escapeAttrib(rewrite));
        }
        emit('"');
      }
      emit('>');
    },
    'endTag': function (tagName) {
      if (ignore > 0) {
        ignore--;
        return;
      }
      emit('</');
      emit(tagName);
      emit('>');
    },
    'pcdata': emit,
    'rcdata': emit,
    'cdata': emit,
  });
  parser(html);
  return output.join('');
}
function sanitizeHtml ( html , doc ) { const tagPolicy = htmlSanitizer . makeTagPolicy ( ( parsed ) => parsed . getScheme ( ) === 'https' ? parsed : null ) ; const output = [ ] ; let ignore = 0 ; const emit = ( content ) => { if ( ignore == 0 ) { output . push ( content ) ; } } ; const cajaDenylistedTags = { 'script' : true , 'svg' : true , ... DENYLISTED_TAGS , } ; const parser = htmlSanitizer . makeSaxParser ( { 'startTag' : function ( tagName , attribs ) { if ( ignore > 0 ) { if ( ! SELF_CLOSING_TAGS [ tagName ] ) { ignore ++ ; } return ; } const isAmpElement = tagName . startsWith ( 'amp-' ) ; const bindingAttribs = [ ] ; for ( let i = 0 ; i < attribs . length ; i += 2 ) { const attr = attribs [ i ] ; if ( ! attr ) { continue ; } const classicBinding = attr [ 0 ] == '[' && attr [ attr . length - 1 ] == ']' ; const alternativeBinding = attr . startsWith ( BIND_PREFIX ) ; if ( classicBinding ) { attribs [ i ] = attr . slice ( 1 , - 1 ) ; } if ( classicBinding || alternativeBinding ) { bindingAttribs . push ( i ) ; } } if ( cajaDenylistedTags [ tagName ] ) { ignore ++ ; } else if ( isAmpElement ) { if ( isAmp4Email ( doc ) && ! EMAIL_ALLOWLISTED_AMP_TAGS [ tagName ] ) { ignore ++ ; } } else { const savedAttribs = attribs . slice ( 0 ) ; const scrubbed = ( tagPolicy ( tagName , attribs ) ) ; if ( ! scrubbed ) { ignore ++ ; } else { attribs = scrubbed [ 'attribs' ] ; for ( let i = 0 ; i < attribs . length ; i += 2 ) { const attrName = attribs [ i ] ; if ( ALLOWLISTED_ATTRS . includes ( attrName ) ) { attribs [ i + 1 ] = savedAttribs [ i + 1 ] ; } else if ( attrName . search ( ALLOWLISTED_ATTR_PREFIX_REGEX ) == 0 ) { attribs [ i + 1 ] = savedAttribs [ i + 1 ] ; } else if ( ALLOWLISTED_ATTRS_BY_TAGS [ tagName ] && ALLOWLISTED_ATTRS_BY_TAGS [ tagName ] . includes ( attrName ) ) { attribs [ i + 1 ] = savedAttribs [ i + 1 ] ; } } } if ( tagName == 'a' ) { let index = - 1 ; let hasHref = false ; for ( let i = 0 ; i < savedAttribs . length ; i += 2 ) { if ( savedAttribs [ i ] == 'target' ) { index = i + 1 ; } else if ( savedAttribs [ i ] == 'href' ) { hasHref = attribs [ i + 1 ] != null ; } } let origTarget = index != - 1 ? savedAttribs [ index ] : null ; if ( origTarget != null ) { origTarget = origTarget . toLowerCase ( ) ; if ( ALLOWLISTED_TARGETS . indexOf ( origTarget ) != - 1 ) { attribs [ index ] = origTarget ; } else { attribs [ index ] = '_top' ; } } else if ( hasHref ) { attribs . push ( 'target' , '_top' ) ; } } } if ( ignore > 0 ) { if ( SELF_CLOSING_TAGS [ tagName ] ) { ignore -- ; } return ; } const hasBindings = bindingAttribs . some ( ( i ) => ! ! attribs [ i + 1 ] ) ; if ( hasBindings ) { attribs . push ( 'i-amphtml-binding' , '' ) ; } emit ( '<' ) ; emit ( tagName ) ; for ( let i = 0 ; i < attribs . length ; i += 2 ) { const attrName = attribs [ i ] ; const attrValue = attribs [ i + 1 ] ; if ( ! isValidAttr ( tagName , attrName , attrValue , doc , false ) ) { user ( ) . error ( TAG , ` ${ attrName } ` + ` ${ tagName } ${ attrName } ${ attrValue } ` ) ; continue ; } emit ( ' ' ) ; if ( bindingAttribs . includes ( i ) && ! attrName . startsWith ( BIND_PREFIX ) ) { emit ( ` ${ attrName } ` ) ; } else { emit ( attrName ) ; } emit ( '="' ) ; if ( attrValue ) { const rewrite = bindingAttribs . includes ( i ) ? attrValue : rewriteAttributeValue ( tagName , attrName , attrValue ) ; emit ( htmlSanitizer . escapeAttrib ( rewrite ) ) ; } emit ( '"' ) ; } emit ( '>' ) ; } , 'endTag' : function ( tagName ) { if ( ignore > 0 ) { ignore -- ; return ; } emit ( '</' ) ; emit ( tagName ) ; emit ( '>' ) ; } , 'pcdata' : emit , 'rcdata' : emit , 'cdata' : emit , } ) ; parser ( html ) ; return output . join ( '' ) ; }
Sanitizes the provided HTML.
Sanitizes the provided HTML .
// No Caja support for <script> or <svg>. // Preprocess "binding" attributes, e.g. [attr], by stripping enclosing // brackets before custom validation and restoring them afterwards. // Enforce AMP4EMAIL tag allowlist at runtime. // Ask Caja to validate the element as well. // Use the resulting properties. /** @type {!JsonObject} */ // Restore some of the attributes that AMP is directly responsible // for, such as "on". // `<A>` has special target rules: // - Default target is "_top"; // - Allowed targets are "_blank", "_top"; // - All other targets are rewritted to "_top". // Only allow valid `href` values. // Filter out bindings with empty attribute values. // Set a custom attribute to identify elements with bindings. // This is an optimization that avoids the need for a DOM scan later. // Rewrite attribute values unless this attribute is a binding. // Bindings contain expressions and shouldn't be rewritten.