safeurl_test.js

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Unit tests for goog.html.SafeUrl and its builders.
 */

goog.provide('goog.html.safeUrlTest');

goog.require('goog.html.SafeUrl');
goog.require('goog.i18n.bidi.Dir');
goog.require('goog.string.Const');
goog.require('goog.testing.jsunit');

goog.setTestOnly('goog.html.safeUrlTest');



function testSafeUrl() {
  var safeUrl = goog.html.SafeUrl.fromConstant(
      goog.string.Const.from('javascript:trusted();'));
  var extracted = goog.html.SafeUrl.unwrap(safeUrl);
  assertEquals('javascript:trusted();', extracted);
  assertEquals('javascript:trusted();', safeUrl.getTypedStringValue());
  assertEquals('SafeUrl{javascript:trusted();}', String(safeUrl));

  // URLs are always LTR.
  assertEquals(goog.i18n.bidi.Dir.LTR, safeUrl.getDirection());

  // Interface markers are present.
  assertTrue(safeUrl.implementsGoogStringTypedString);
  assertTrue(safeUrl.implementsGoogI18nBidiDirectionalString);
}


/** @suppress {checkTypes} */
function testUnwrap() {
  var evil = {};
  evil.safeUrlValueWithSecurityContract_googHtmlSecurityPrivate_ =
      '<script>evil()</script';
  evil.SAFE_URL_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};

  var exception = assertThrows(function() {
    goog.html.SafeUrl.unwrap(evil);
  });
  assertTrue(exception.message.indexOf('expected object of type SafeUrl') > 0);
}


/**
 * Assert that url passes through sanitization unchanged.
 * @param {string|!goog.string.TypedString} url The URL to sanitize.
 */
function assertGoodUrl(url) {
  var expected = url;
  if (url.implementsGoogStringTypedString) {
    expected = url.getTypedStringValue();
  }
  var safeUrl = goog.html.SafeUrl.sanitize(url);
  var extracted = goog.html.SafeUrl.unwrap(safeUrl);
  assertEquals(expected, extracted);
}


/**
 * Assert that url fails sanitization.
 * @param {string|!goog.string.TypedString} url The URL to sanitize.
 */
function assertBadUrl(url) {
  assertEquals(
      goog.html.SafeUrl.INNOCUOUS_STRING,
      goog.html.SafeUrl.unwrap(
          goog.html.SafeUrl.sanitize(url)));
}


function testSafeUrlSanitize_validatesUrl() {
  // Whitelisted schemes.
  assertGoodUrl('http://example.com/');
  assertGoodUrl('https://example.com');
  assertGoodUrl('mailto:foo@example.com');
  // Scheme is case-insensitive
  assertGoodUrl('HTtp://example.com/');
  // Different URL components go through.
  assertGoodUrl('https://example.com/path?foo=bar#baz');
  // Scheme-less URL with authority.
  assertGoodUrl('//example.com/path');
  // Absolute path with no authority.
  assertGoodUrl('/path');
  assertGoodUrl('/path?foo=bar#baz');
  // Relative path.
  assertGoodUrl('path');
  assertGoodUrl('path?foo=bar#baz');
  assertGoodUrl('p//ath');
  assertGoodUrl('p//ath?foo=bar#baz');
  // Restricted characters ('&', ':', \') after [/?#].
  assertGoodUrl('/&');
  assertGoodUrl('?:');

  // .sanitize() works on program constants.
  assertGoodUrl(goog.string.Const.from('http://example.com/'));

  // Non-whitelisted schemes.
  assertBadUrl('javascript:evil();');
  assertBadUrl('javascript:evil();//\nhttp://good.com/');
  assertBadUrl('data:blah');
  // Restricted characters before [/?#].
  assertBadUrl('&');
  assertBadUrl(':');
  // '\' is not treated like '/': no restricted characters allowed after it.
  assertBadUrl('\\:');
  // Regex anchored to the left: doesn't match on '/:'.
  assertBadUrl(':/:');
  // Regex multiline not enabled: first line would match but second one
  // wouldn't.
  assertBadUrl('path\n:');

  // .sanitize() does not exempt values known to be program constants.
  assertBadUrl(goog.string.Const.from('data:blah'));
}


/**
 * Asserts that goog.html.SafeUrl.unwrap returns the expected string when the
 * SafeUrl has been constructed by passing the given url to
 * goog.html.SafeUrl.sanitize.
 * @param {string} url The string to pass to goog.html.SafeUrl.sanitize.
 * @param {string} expected The string representation that
 *         goog.html.SafeUrl.unwrap should return.
 */
function assertSanitizeEncodesTo(url, expected) {
  var safeUrl = goog.html.SafeUrl.sanitize(url);
  var actual = goog.html.SafeUrl.unwrap(safeUrl);
  assertEquals(
      'SafeUrl.sanitize().unwrap() doesn\'t return expected ' +
          'percent-encoded string',
      expected,
      actual);
}


function testSafeUrlSanitize_percentEncodesUrl() {
  // '%' is preserved.
  assertSanitizeEncodesTo('%', '%');
  assertSanitizeEncodesTo('%2F', '%2F');

  // Unreserved characters, RFC 3986.
  assertSanitizeEncodesTo('aA1-._~', 'aA1-._~');

  // Reserved characters, RFC 3986. Only '\'', '(' and ')' are encoded.
  assertSanitizeEncodesTo('/:?#[]@!$&\'()*+,;=', '/:?#[]@!$&%27%28%29*+,;=');


  // Other ASCII characters, printable and non-printable.
  assertSanitizeEncodesTo('^"\\`\x00\n\r\x7f', '%5E%22%5C%60%00%0A%0D%7F');

  // Codepoints which UTF-8 encode to 2 bytes.
  assertSanitizeEncodesTo('\u0080\u07ff', '%C2%80%DF%BF');

  // Highest codepoint which can be UTF-16 encoded using two bytes
  // (one code unit). Highest codepoint in basic multilingual plane and highest
  // that JavaScript can represent using \u.
  assertSanitizeEncodesTo('\uffff', '%EF%BF%BF');

  // Supplementary plane codepoint which UTF-16 and UTF-8 encode to 4 bytes.
  // Valid surrogate sequence.
  assertSanitizeEncodesTo('\ud800\udc00', '%F0%90%80%80');

  // Invalid lead/high surrogate.
  assertSanitizeEncodesTo('\udc00', goog.html.SafeUrl.INNOCUOUS_STRING);

  // Invalid trail/low surrogate.
  assertSanitizeEncodesTo('\ud800\ud800', goog.html.SafeUrl.INNOCUOUS_STRING);
}


function testSafeUrlSanitize_idempotentForSafeUrlArgument() {
  // This goes through percent-encoding.
  var safeUrl = goog.html.SafeUrl.sanitize('%11"');
  var safeUrl2 = goog.html.SafeUrl.sanitize(safeUrl);
  assertEquals(
      goog.html.SafeUrl.unwrap(safeUrl), goog.html.SafeUrl.unwrap(safeUrl2));

  // This doesn't match the safe prefix, getting converted into an innocuous
  // string.
  safeUrl = goog.html.SafeUrl.sanitize('disallowed:foo');
  safeUrl2 = goog.html.SafeUrl.sanitize(safeUrl);
  assertEquals(
      goog.html.SafeUrl.unwrap(safeUrl), goog.html.SafeUrl.unwrap(safeUrl2));
}