Skip to content
This repository was archived by the owner on Aug 1, 2024. It is now read-only.

Commit fe511fa

Browse files
Closure Teamcopybara-github
authored andcommitted
Defines and URL sanitizer that only blocks javascript: URLs.
RELNOTES: Defines and URL sanitizer that only blocks javascript: URLs. PiperOrigin-RevId: 500960110 Change-Id: I1491fb817418cffc7687457fac13f8ae131f684d
1 parent d407e5c commit fe511fa

File tree

4 files changed

+144
-1
lines changed

4 files changed

+144
-1
lines changed

closure/goog/html/BUILD

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,10 @@ closure_js_library(
117117
closure_js_library(
118118
name = "safeurl_test_vectors",
119119
testonly = 1,
120-
srcs = ["safeurl_test_vectors.js"],
120+
srcs = [
121+
"javascript_url_test_vectors.js",
122+
"safeurl_test_vectors.js",
123+
],
121124
lenient = True,
122125
)
123126

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @license
3+
* Copyright The Closure Library Authors.
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
// AUTOGENERATED. DO NOT EDIT.
8+
// clang-format off
9+
10+
goog.module('goog.html.javascriptUrlTestVectors');
11+
goog.setTestOnly('goog.html.javascriptUrlTestVectors');
12+
13+
/** @typedef {{input: string, expected: string, safe: boolean }} */
14+
let TestVector;
15+
16+
/** @const {!Array<!TestVector>} */
17+
const BASE_VECTORS = [
18+
{input: '', expected: '', safe: true},
19+
{input: 'http://example.com/', expected: 'http://example.com/', safe: true},
20+
{input: 'https://example.com', expected: 'https://example.com', safe: true},
21+
{input: 'mailto:[email protected]', expected: 'mailto:[email protected]', safe: true},
22+
{input: 'ftp://example.com', expected: 'ftp://example.com', safe: true},
23+
{input: 'ftp://[email protected]', expected: 'ftp://[email protected]', safe: true},
24+
{input: 'ftp://username:[email protected]', expected: 'ftp://username:[email protected]', safe: true},
25+
{input: 'HTtp://example.com/', expected: 'HTtp://example.com/', safe: true},
26+
{input: 'https://example.com/path?foo\u003Dbar#baz', expected: 'https://example.com/path?foo\u003Dbar#baz', safe: true},
27+
{input: 'https://example.com:123/path?foo\u003Dbar\u0026abc\u003Ddef#baz', expected: 'https://example.com:123/path?foo\u003Dbar\u0026abc\u003Ddef#baz', safe: true},
28+
{input: '//example.com/path', expected: '//example.com/path', safe: true},
29+
{input: '/path', expected: '/path', safe: true},
30+
{input: '/path?foo\u003Dbar#baz', expected: '/path?foo\u003Dbar#baz', safe: true},
31+
{input: 'path', expected: 'path', safe: true},
32+
{input: 'path?foo\u003Dbar#baz', expected: 'path?foo\u003Dbar#baz', safe: true},
33+
{input: 'p//ath', expected: 'p//ath', safe: true},
34+
{input: 'p//ath?foo\u003Dbar#baz', expected: 'p//ath?foo\u003Dbar#baz', safe: true},
35+
{input: '#baz', expected: '#baz', safe: true},
36+
{input: '?:', expected: '?:', safe: true},
37+
{input: 'not-data:image/png;base64,z\u003D', expected: 'not-data:image/png;base64,z\u003D', safe: true},
38+
{input: ' data:image/png;base64,z\u003D', expected: ' data:image/png;base64,z\u003D', safe: true},
39+
{input: 'tel:+1234567890', expected: 'tel:+1234567890', safe: true},
40+
{input: 'sms:+1234567890', expected: 'sms:+1234567890', safe: true},
41+
{input: 'callto:+1234567890', expected: 'callto:+1234567890', safe: true},
42+
{input: 'wtai://wp/mc;+1234567890', expected: 'wtai://wp/mc;+1234567890', safe: true},
43+
{input: 'rtsp://example.org/', expected: 'rtsp://example.org/', safe: true},
44+
{input: 'market://details?id\u003Dapp', expected: 'market://details?id\u003Dapp', safe: true},
45+
{input: 'itms://itunes.apple.com/us', expected: 'itms://itunes.apple.com/us', safe: true},
46+
{input: 'javascript:evil(1);', expected: 'about:invalid#zClosurez', safe: false},
47+
{input: 'javascript:evil(2);//\u000Ahttp://good.com/', expected: 'about:invalid#zClosurez', safe: false},
48+
{input: ' javascript:evil(3);', expected: 'about:invalid#zClosurez', safe: false},
49+
{input: '\u0009javascript:evil(4);', expected: 'about:invalid#zClosurez', safe: false},
50+
{input: '\u000Bjavascript:evil(5);', expected: 'about:invalid#zClosurez', safe: false},
51+
{input: 'JaVasCriPT:evil(6);', expected: 'about:invalid#zClosurez', safe: false},
52+
{input: 'javascript:evil(8);', expected: 'about:invalid#zClosurez', safe: false},
53+
{input: 'javascript:evil(9);', expected: 'about:invalid#zClosurez', safe: false},
54+
{input: 'javasc\u0009ript:evil(10);', expected: 'about:invalid#zClosurez', safe: false},
55+
{input: 'javasc\u0009ript:evil(11);', expected: 'about:invalid#zClosurez', safe: false}
56+
];
57+
58+
/** @const {!Array<!TestVector>} */
59+
const TEL_VECTORS = [
60+
];
61+
62+
/** @const {!Array<!TestVector>} */
63+
const SMS_VECTORS = [
64+
];
65+
66+
/** @const {!Array<!TestVector>} */
67+
const SSH_VECTORS = [
68+
];
69+
70+
exports = {BASE_VECTORS, TEL_VECTORS, SMS_VECTORS, SSH_VECTORS};

closure/goog/html/safeurl.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,58 @@ goog.html.SafeUrl.sanitizeAssertUnchanged = function(url, opt_allowDataUrl) {
741741
return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(url);
742742
};
743743

744+
/**
745+
* Extracts the scheme from the given URL. If the URL is relative, https: is
746+
* assumed.
747+
* @param {string} url The URL to extract the scheme from.
748+
* @return {string|undefined} the URL scheme.
749+
*/
750+
goog.html.SafeUrl.extractScheme = function(url) {
751+
let parsedUrl;
752+
try {
753+
parsedUrl = new URL(url);
754+
} catch (e) {
755+
// According to https://url.spec.whatwg.org/#constructors, the URL
756+
// constructor with one parameter throws if `url` is not absolute. In this
757+
// case, we are sure that no explicit scheme (javascript: ) is set.
758+
// This can also be a URL parsing error, but in this case the URL won't be
759+
// run anyway.
760+
return 'https:';
761+
}
762+
return parsedUrl.protocol;
763+
};
764+
765+
/**
766+
* Creates a SafeUrl object from `url`. If `url` is a
767+
* `goog.html.SafeUrl` then it is simply returned. Otherwise javascript: URLs
768+
* are rejected.
769+
*
770+
* This function asserts (using goog.asserts) that the URL scheme is not
771+
* javascript. If it is, in addition to failing the assert, an innocuous URL
772+
* will be returned.
773+
*
774+
* @see http://url.spec.whatwg.org/#concept-relative-url
775+
* @param {string|!goog.string.TypedString} url The URL to validate.
776+
* @return {!goog.html.SafeUrl} The validated URL, wrapped as a SafeUrl.
777+
*/
778+
goog.html.SafeUrl.sanitizeJavascriptUrlAssertUnchanged = function(url) {
779+
'use strict';
780+
if (url instanceof goog.html.SafeUrl) {
781+
return url;
782+
} else if (typeof url == 'object' && url.implementsGoogStringTypedString) {
783+
url = /** @type {!goog.string.TypedString} */ (url).getTypedStringValue();
784+
} else {
785+
url = String(url);
786+
}
787+
// We don't rely on goog.url here to prevent a dependency cycle.
788+
const parsedScheme = goog.html.SafeUrl.extractScheme(url);
789+
if (!goog.asserts.assert(
790+
parsedScheme !== 'javascript:', '%s is a javascript: URL', url)) {
791+
url = goog.html.SafeUrl.INNOCUOUS_STRING;
792+
}
793+
return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(url);
794+
};
795+
744796
/**
745797
* Token used to ensure that object is created only from this file. No code
746798
* outside of this file can access this token.

closure/goog/html/safeurl_test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const SafeUrl = goog.require('goog.html.SafeUrl');
1515
const TrustedResourceUrl = goog.require('goog.html.TrustedResourceUrl');
1616
const fsUrl = goog.require('goog.fs.url');
1717
const googObject = goog.require('goog.object');
18+
const javascriptUrlTestVectors = goog.require('goog.html.javascriptUrlTestVectors');
1819
const safeUrlTestVectors = goog.require('goog.html.safeUrlTestVectors');
1920
const testSuite = goog.require('goog.testing.testSuite');
2021
const {assertExists} = goog.require('goog.asserts');
@@ -382,6 +383,23 @@ testSuite({
382383
}
383384
},
384385

386+
/**
387+
@suppress {missingProperties,checkTypes} suppression added to enable type
388+
checking
389+
*/
390+
testSafeUrlSanitize_sanitizeJavascriptUrlAssertUnchanged() {
391+
for (const v of javascriptUrlTestVectors.BASE_VECTORS) {
392+
if (v.safe) {
393+
const asserted = SafeUrl.sanitizeJavascriptUrlAssertUnchanged(v.input);
394+
assertEquals(v.expected, SafeUrl.unwrap(asserted));
395+
} else {
396+
assertThrows(() => {
397+
SafeUrl.sanitizeJavascriptUrlAssertUnchanged(v.input);
398+
});
399+
}
400+
}
401+
},
402+
385403
testSafeUrlSanitize_sanitizeProgramConstants() {
386404
// .sanitize() works on program constants.
387405
const good = Const.from('http://example.com/');

0 commit comments

Comments
 (0)