Skip to content

Commit

Permalink
Defines and URL sanitizer that only blocks javascript: URLs.
Browse files Browse the repository at this point in the history
RELNOTES: Defines and URL sanitizer that only blocks javascript: URLs.

PiperOrigin-RevId: 500960110
Change-Id: I1491fb817418cffc7687457fac13f8ae131f684d
  • Loading branch information
Closure Team authored and Copybara-Service committed Jan 10, 2023
1 parent d407e5c commit fe511fa
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 1 deletion.
5 changes: 4 additions & 1 deletion closure/goog/html/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ closure_js_library(
closure_js_library(
name = "safeurl_test_vectors",
testonly = 1,
srcs = ["safeurl_test_vectors.js"],
srcs = [
"javascript_url_test_vectors.js",
"safeurl_test_vectors.js",
],
lenient = True,
)

Expand Down
70 changes: 70 additions & 0 deletions closure/goog/html/javascript_url_test_vectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/

// AUTOGENERATED. DO NOT EDIT.
// clang-format off

goog.module('goog.html.javascriptUrlTestVectors');
goog.setTestOnly('goog.html.javascriptUrlTestVectors');

/** @typedef {{input: string, expected: string, safe: boolean }} */
let TestVector;

/** @const {!Array<!TestVector>} */
const BASE_VECTORS = [
{input: '', expected: '', safe: true},
{input: 'http://example.com/', expected: 'http://example.com/', safe: true},
{input: 'https://example.com', expected: 'https://example.com', safe: true},
{input: 'mailto:foo@example.com', expected: 'mailto:foo@example.com', safe: true},
{input: 'ftp://example.com', expected: 'ftp://example.com', safe: true},
{input: 'ftp://username@example.com', expected: 'ftp://username@example.com', safe: true},
{input: 'ftp://username:password@example.com', expected: 'ftp://username:password@example.com', safe: true},
{input: 'HTtp://example.com/', expected: 'HTtp://example.com/', safe: true},
{input: 'https://example.com/path?foo\u003Dbar#baz', expected: 'https://example.com/path?foo\u003Dbar#baz', safe: true},
{input: 'https://example.com:123/path?foo\u003Dbar\u0026abc\u003Ddef#baz', expected: 'https://example.com:123/path?foo\u003Dbar\u0026abc\u003Ddef#baz', safe: true},
{input: '//example.com/path', expected: '//example.com/path', safe: true},
{input: '/path', expected: '/path', safe: true},
{input: '/path?foo\u003Dbar#baz', expected: '/path?foo\u003Dbar#baz', safe: true},
{input: 'path', expected: 'path', safe: true},
{input: 'path?foo\u003Dbar#baz', expected: 'path?foo\u003Dbar#baz', safe: true},
{input: 'p//ath', expected: 'p//ath', safe: true},
{input: 'p//ath?foo\u003Dbar#baz', expected: 'p//ath?foo\u003Dbar#baz', safe: true},
{input: '#baz', expected: '#baz', safe: true},
{input: '?:', expected: '?:', safe: true},
{input: 'not-data:image/png;base64,z\u003D', expected: 'not-data:image/png;base64,z\u003D', safe: true},
{input: ' data:image/png;base64,z\u003D', expected: ' data:image/png;base64,z\u003D', safe: true},
{input: 'tel:+1234567890', expected: 'tel:+1234567890', safe: true},
{input: 'sms:+1234567890', expected: 'sms:+1234567890', safe: true},
{input: 'callto:+1234567890', expected: 'callto:+1234567890', safe: true},
{input: 'wtai://wp/mc;+1234567890', expected: 'wtai://wp/mc;+1234567890', safe: true},
{input: 'rtsp://example.org/', expected: 'rtsp://example.org/', safe: true},
{input: 'market://details?id\u003Dapp', expected: 'market://details?id\u003Dapp', safe: true},
{input: 'itms://itunes.apple.com/us', expected: 'itms://itunes.apple.com/us', safe: true},
{input: 'javascript:evil(1);', expected: 'about:invalid#zClosurez', safe: false},
{input: 'javascript:evil(2);//\u000Ahttp://good.com/', expected: 'about:invalid#zClosurez', safe: false},
{input: ' javascript:evil(3);', expected: 'about:invalid#zClosurez', safe: false},
{input: '\u0009javascript:evil(4);', expected: 'about:invalid#zClosurez', safe: false},
{input: '\u000Bjavascript:evil(5);', expected: 'about:invalid#zClosurez', safe: false},
{input: 'JaVasCriPT:evil(6);', expected: 'about:invalid#zClosurez', safe: false},
{input: 'javascript:evil(8);', expected: 'about:invalid#zClosurez', safe: false},
{input: 'javascript:evil(9);', expected: 'about:invalid#zClosurez', safe: false},
{input: 'javasc\u0009ript:evil(10);', expected: 'about:invalid#zClosurez', safe: false},
{input: 'javasc\u0009ript:evil(11);', expected: 'about:invalid#zClosurez', safe: false}
];

/** @const {!Array<!TestVector>} */
const TEL_VECTORS = [
];

/** @const {!Array<!TestVector>} */
const SMS_VECTORS = [
];

/** @const {!Array<!TestVector>} */
const SSH_VECTORS = [
];

exports = {BASE_VECTORS, TEL_VECTORS, SMS_VECTORS, SSH_VECTORS};
52 changes: 52 additions & 0 deletions closure/goog/html/safeurl.js
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,58 @@ goog.html.SafeUrl.sanitizeAssertUnchanged = function(url, opt_allowDataUrl) {
return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(url);
};

/**
* Extracts the scheme from the given URL. If the URL is relative, https: is
* assumed.
* @param {string} url The URL to extract the scheme from.
* @return {string|undefined} the URL scheme.
*/
goog.html.SafeUrl.extractScheme = function(url) {
let parsedUrl;
try {
parsedUrl = new URL(url);
} catch (e) {
// According to https://url.spec.whatwg.org/#constructors, the URL
// constructor with one parameter throws if `url` is not absolute. In this
// case, we are sure that no explicit scheme (javascript: ) is set.
// This can also be a URL parsing error, but in this case the URL won't be
// run anyway.
return 'https:';
}
return parsedUrl.protocol;
};

/**
* Creates a SafeUrl object from `url`. If `url` is a
* `goog.html.SafeUrl` then it is simply returned. Otherwise javascript: URLs
* are rejected.
*
* This function asserts (using goog.asserts) that the URL scheme is not
* javascript. If it is, in addition to failing the assert, an innocuous URL
* will be returned.
*
* @see http://url.spec.whatwg.org/#concept-relative-url
* @param {string|!goog.string.TypedString} url The URL to validate.
* @return {!goog.html.SafeUrl} The validated URL, wrapped as a SafeUrl.
*/
goog.html.SafeUrl.sanitizeJavascriptUrlAssertUnchanged = function(url) {
'use strict';
if (url instanceof goog.html.SafeUrl) {
return url;
} else if (typeof url == 'object' && url.implementsGoogStringTypedString) {
url = /** @type {!goog.string.TypedString} */ (url).getTypedStringValue();
} else {
url = String(url);
}
// We don't rely on goog.url here to prevent a dependency cycle.
const parsedScheme = goog.html.SafeUrl.extractScheme(url);
if (!goog.asserts.assert(
parsedScheme !== 'javascript:', '%s is a javascript: URL', url)) {
url = goog.html.SafeUrl.INNOCUOUS_STRING;
}
return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(url);
};

/**
* Token used to ensure that object is created only from this file. No code
* outside of this file can access this token.
Expand Down
18 changes: 18 additions & 0 deletions closure/goog/html/safeurl_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const SafeUrl = goog.require('goog.html.SafeUrl');
const TrustedResourceUrl = goog.require('goog.html.TrustedResourceUrl');
const fsUrl = goog.require('goog.fs.url');
const googObject = goog.require('goog.object');
const javascriptUrlTestVectors = goog.require('goog.html.javascriptUrlTestVectors');
const safeUrlTestVectors = goog.require('goog.html.safeUrlTestVectors');
const testSuite = goog.require('goog.testing.testSuite');
const {assertExists} = goog.require('goog.asserts');
Expand Down Expand Up @@ -382,6 +383,23 @@ testSuite({
}
},

/**
@suppress {missingProperties,checkTypes} suppression added to enable type
checking
*/
testSafeUrlSanitize_sanitizeJavascriptUrlAssertUnchanged() {
for (const v of javascriptUrlTestVectors.BASE_VECTORS) {
if (v.safe) {
const asserted = SafeUrl.sanitizeJavascriptUrlAssertUnchanged(v.input);
assertEquals(v.expected, SafeUrl.unwrap(asserted));
} else {
assertThrows(() => {
SafeUrl.sanitizeJavascriptUrlAssertUnchanged(v.input);
});
}
}
},

testSafeUrlSanitize_sanitizeProgramConstants() {
// .sanitize() works on program constants.
const good = Const.from('http://example.com/');
Expand Down

0 comments on commit fe511fa

Please sign in to comment.