Skip to content

Commit 5aefcb4

Browse files
committed
Use safevalues to fix trusted types issues reported by tsec
1 parent 436331a commit 5aefcb4

File tree

14 files changed

+76
-146
lines changed

14 files changed

+76
-146
lines changed

packages/analytics/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,20 @@
4141
"@firebase/app": "0.x"
4242
},
4343
"dependencies": {
44+
"@firebase/component": "0.6.7",
4445
"@firebase/installations": "0.6.7",
4546
"@firebase/logger": "0.4.2",
4647
"@firebase/util": "1.9.6",
47-
"@firebase/component": "0.6.7",
48+
"safevalues": "^0.5.2",
4849
"tslib": "^2.1.0"
4950
},
5051
"license": "Apache-2.0",
5152
"devDependencies": {
5253
"@firebase/app": "0.10.5",
53-
"rollup": "2.79.1",
5454
"@rollup/plugin-commonjs": "21.1.0",
5555
"@rollup/plugin-json": "4.1.0",
5656
"@rollup/plugin-node-resolve": "13.3.0",
57+
"rollup": "2.79.1",
5758
"rollup-plugin-typescript2": "0.31.2",
5859
"typescript": "4.7.4"
5960
},

packages/analytics/src/helpers.test.ts

Lines changed: 2 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,12 @@ import {
2424
insertScriptTag,
2525
wrapOrCreateGtag,
2626
findGtagScriptOnPage,
27-
promiseAllSettled,
28-
createGtagTrustedTypesScriptURL,
29-
createTrustedTypesPolicy
27+
promiseAllSettled
3028
} from './helpers';
31-
import { GtagCommand, GTAG_URL } from './constants';
29+
import { GtagCommand } from './constants';
3230
import { Deferred } from '@firebase/util';
3331
import { ConsentSettings } from './public-types';
3432
import { removeGtagScripts } from '../testing/gtag-script-util';
35-
import { logger } from './logger';
36-
import { AnalyticsError, ERROR_FACTORY } from './errors';
3733

3834
const fakeMeasurementId = 'abcd-efgh-ijkl';
3935
const fakeAppId = 'my-test-app-1234';
@@ -50,71 +46,6 @@ const fakeDynamicConfig: DynamicConfig = {
5046
};
5147
const fakeDynamicConfigPromises = [Promise.resolve(fakeDynamicConfig)];
5248

53-
describe('Trusted Types policies and functions', () => {
54-
if (window.trustedTypes) {
55-
describe('Trusted types exists', () => {
56-
let ttStub: SinonStub;
57-
58-
beforeEach(() => {
59-
ttStub = stub(
60-
window.trustedTypes as TrustedTypePolicyFactory,
61-
'createPolicy'
62-
).returns({
63-
createScriptURL: (s: string) => s
64-
} as any);
65-
});
66-
67-
afterEach(() => {
68-
removeGtagScripts();
69-
ttStub.restore();
70-
});
71-
72-
it('Verify trustedTypes is called if the API is available', () => {
73-
const trustedTypesPolicy = createTrustedTypesPolicy(
74-
'firebase-js-sdk-policy',
75-
{
76-
createScriptURL: createGtagTrustedTypesScriptURL
77-
}
78-
);
79-
80-
expect(ttStub).to.be.called;
81-
expect(trustedTypesPolicy).not.to.be.undefined;
82-
});
83-
84-
it('createGtagTrustedTypesScriptURL verifies gtag URL base exists when a URL is provided', () => {
85-
expect(createGtagTrustedTypesScriptURL(GTAG_URL)).to.equal(GTAG_URL);
86-
});
87-
88-
it('createGtagTrustedTypesScriptURL rejects URLs with non-gtag base', () => {
89-
const NON_GTAG_URL = 'http://iamnotgtag.com';
90-
const loggerWarnStub = stub(logger, 'warn');
91-
const errorMessage = ERROR_FACTORY.create(
92-
AnalyticsError.INVALID_GTAG_RESOURCE,
93-
{
94-
gtagURL: NON_GTAG_URL
95-
}
96-
).message;
97-
98-
expect(createGtagTrustedTypesScriptURL(NON_GTAG_URL)).to.equal('');
99-
expect(loggerWarnStub).to.be.calledWith(errorMessage);
100-
});
101-
});
102-
}
103-
describe('Trusted types does not exist', () => {
104-
it('Verify trustedTypes functions are not called if the API is not available', () => {
105-
delete window.trustedTypes;
106-
const trustedTypesPolicy = createTrustedTypesPolicy(
107-
'firebase-js-sdk-policy',
108-
{
109-
createScriptURL: createGtagTrustedTypesScriptURL
110-
}
111-
);
112-
113-
expect(trustedTypesPolicy).to.be.undefined;
114-
});
115-
});
116-
});
117-
11849
describe('Gtag wrapping functions', () => {
11950
afterEach(() => {
12051
removeGtagScripts();

packages/analytics/src/helpers.ts

Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,12 @@ import {
2424
import { DynamicConfig, DataLayer, Gtag, MinimalDynamicConfig } from './types';
2525
import { GtagCommand, GTAG_URL } from './constants';
2626
import { logger } from './logger';
27-
import { AnalyticsError, ERROR_FACTORY } from './errors';
27+
import { trustedResourceUrl } from 'safevalues';
28+
import { safeScriptEl } from 'safevalues/dom';
2829

2930
// Possible parameter types for gtag 'event' and 'config' commands
3031
type GtagConfigOrEventParams = ControlParams & EventParams & CustomParams;
3132

32-
/**
33-
* Verifies and creates a TrustedScriptURL.
34-
*/
35-
export function createGtagTrustedTypesScriptURL(url: string): string {
36-
if (!url.startsWith(GTAG_URL)) {
37-
const err = ERROR_FACTORY.create(AnalyticsError.INVALID_GTAG_RESOURCE, {
38-
gtagURL: url
39-
});
40-
logger.warn(err.message);
41-
return '';
42-
}
43-
return url;
44-
}
45-
4633
/**
4734
* Makeshift polyfill for Promise.allSettled(). Resolves when all promises
4835
* have either resolved or rejected.
@@ -55,29 +42,6 @@ export function promiseAllSettled<T>(
5542
return Promise.all(promises.map(promise => promise.catch(e => e)));
5643
}
5744

58-
/**
59-
* Creates a TrustedTypePolicy object that implements the rules passed as policyOptions.
60-
*
61-
* @param policyName A string containing the name of the policy
62-
* @param policyOptions Object containing implementations of instance methods for TrustedTypesPolicy, see {@link https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy#instance_methods
63-
* | the TrustedTypePolicy reference documentation}.
64-
*/
65-
export function createTrustedTypesPolicy(
66-
policyName: string,
67-
policyOptions: Partial<TrustedTypePolicyOptions>
68-
): Partial<TrustedTypePolicy> | undefined {
69-
// Create a TrustedTypes policy that we can use for updating src
70-
// properties
71-
let trustedTypesPolicy: Partial<TrustedTypePolicy> | undefined;
72-
if (window.trustedTypes) {
73-
trustedTypesPolicy = window.trustedTypes.createPolicy(
74-
policyName,
75-
policyOptions
76-
);
77-
}
78-
return trustedTypesPolicy;
79-
}
80-
8145
/**
8246
* Inserts gtag script tag into the page to asynchronously download gtag.
8347
* @param dataLayerName Name of datalayer (most often the default, "_dataLayer").
@@ -86,21 +50,17 @@ export function insertScriptTag(
8650
dataLayerName: string,
8751
measurementId: string
8852
): void {
89-
const trustedTypesPolicy = createTrustedTypesPolicy(
90-
'firebase-js-sdk-policy',
91-
{
92-
createScriptURL: createGtagTrustedTypesScriptURL
93-
}
94-
);
95-
9653
const script = document.createElement('script');
54+
9755
// We are not providing an analyticsId in the URL because it would trigger a `page_view`
9856
// without fid. We will initialize ga-id using gtag (config) command together with fid.
99-
100-
const gtagScriptURL = `${GTAG_URL}?l=${dataLayerName}&id=${measurementId}`;
101-
(script.src as string | TrustedScriptURL) = trustedTypesPolicy
102-
? (trustedTypesPolicy as TrustedTypePolicy)?.createScriptURL(gtagScriptURL)
103-
: gtagScriptURL;
57+
//
58+
// We also have to ensure the template string before the first expression constitutes a valid URL
59+
// start, as this is what the initial validation focuses on. If the template literal begins
60+
// directly with an expression (e.g. `${GTAG_SCRIPT_URL}`), the validation fails due to an
61+
// empty initial string.
62+
const url = trustedResourceUrl`https://www.googletagmanager.com/gtag/js?l=${dataLayerName}&id=${measurementId}`;
63+
safeScriptEl.setSrc(script, url);
10464

10565
script.async = true;
10666
document.head.appendChild(script);

packages/app-check/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,19 @@
3939
"@firebase/app": "0.x"
4040
},
4141
"dependencies": {
42-
"@firebase/util": "1.9.6",
4342
"@firebase/component": "0.6.7",
4443
"@firebase/logger": "0.4.2",
44+
"@firebase/util": "1.9.6",
45+
"safevalues": "^0.5.2",
4546
"tslib": "^2.1.0"
4647
},
4748
"license": "Apache-2.0",
4849
"devDependencies": {
4950
"@firebase/app": "0.10.5",
50-
"rollup": "2.79.1",
5151
"@rollup/plugin-commonjs": "21.1.0",
5252
"@rollup/plugin-json": "4.1.0",
5353
"@rollup/plugin-node-resolve": "13.3.0",
54+
"rollup": "2.79.1",
5455
"rollup-plugin-typescript2": "0.31.2",
5556
"typescript": "4.7.4"
5657
},

packages/app-check/src/recaptcha.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ import { FirebaseApp } from '@firebase/app';
1919
import { getStateReference } from './state';
2020
import { Deferred } from '@firebase/util';
2121
import { getRecaptcha, ensureActivated } from './util';
22+
import { trustedResourceUrl } from 'safevalues';
23+
import { safeScriptEl } from 'safevalues/dom';
2224

25+
// Note that these are used for testing. If they are changed, the URLs used in loadReCAPTCHAV3Script
26+
// and loadReCAPTCHAEnterpriseScript must also be changed. They aren't used to create the URLs
27+
// since trusted resource URLs must be created using template string literals.
2328
export const RECAPTCHA_URL = 'https://www.google.com/recaptcha/api.js';
2429
export const RECAPTCHA_ENTERPRISE_URL =
2530
'https://www.google.com/recaptcha/enterprise.js';
@@ -166,14 +171,20 @@ function renderInvisibleWidget(
166171

167172
function loadReCAPTCHAV3Script(onload: () => void): void {
168173
const script = document.createElement('script');
169-
script.src = RECAPTCHA_URL;
174+
safeScriptEl.setSrc(
175+
script,
176+
trustedResourceUrl`https://www.google.com/recaptcha/api.js`
177+
);
170178
script.onload = onload;
171179
document.head.appendChild(script);
172180
}
173181

174182
function loadReCAPTCHAEnterpriseScript(onload: () => void): void {
175183
const script = document.createElement('script');
176-
script.src = RECAPTCHA_ENTERPRISE_URL;
184+
safeScriptEl.setSrc(
185+
script,
186+
trustedResourceUrl`https://www.google.com/recaptcha/enterprise.js`
187+
);
177188
script.onload = onload;
178189
document.head.appendChild(script);
179190
}

packages/auth/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,9 @@
130130
"@firebase/component": "0.6.7",
131131
"@firebase/logger": "0.4.2",
132132
"@firebase/util": "1.9.6",
133-
"undici": "5.28.4",
134-
"tslib": "^2.1.0"
133+
"safevalues": "^0.5.2",
134+
"tslib": "^2.1.0",
135+
"undici": "5.28.4"
135136
},
136137
"license": "Apache-2.0",
137138
"devDependencies": {

packages/auth/src/platform_browser/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717

1818
import { FirebaseApp, getApp, _getProvider } from '@firebase/app';
19+
import { safeAttrPrefix } from 'safevalues';
20+
import { safeElement } from 'safevalues/dom';
1921

2022
import {
2123
initializeAuth,
@@ -124,7 +126,7 @@ _setExternalJSProvider({
124126
// TODO: consider adding timeout support & cancellation
125127
return new Promise((resolve, reject) => {
126128
const el = document.createElement('script');
127-
el.setAttribute('src', url);
129+
safeElement.setPrefixedAttribute([safeAttrPrefix`src`], el, 'src', url);
128130
el.onload = resolve;
129131
el.onerror = e => {
130132
const error = _createError(AuthErrorCode.INTERNAL_ERROR);

packages/auth/src/platform_browser/load_js.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import { expect, use } from 'chai';
1919
import * as sinon from 'sinon';
2020
import sinonChai from 'sinon-chai';
21+
import { safeAttrPrefix } from 'safevalues';
22+
import { safeElement } from 'safevalues/dom';
2123

2224
import {
2325
_generateCallbackName,
@@ -44,7 +46,12 @@ describe('platform-browser/load_js', () => {
4446
loadJS(url: string): Promise<Event> {
4547
return new Promise((resolve, reject) => {
4648
const el = document.createElement('script');
47-
el.setAttribute('src', url);
49+
safeElement.setPrefixedAttribute(
50+
[safeAttrPrefix`src`],
51+
el,
52+
'src',
53+
url
54+
);
4855
el.onload = resolve;
4956
el.onerror = e => {
5057
const error = _createError(AuthErrorCode.INTERNAL_ERROR);
@@ -65,7 +72,9 @@ describe('platform-browser/load_js', () => {
6572

6673
// eslint-disable-next-line @typescript-eslint/no-floating-promises
6774
_loadJS('http://localhost/url');
68-
expect(el.setAttribute).to.have.been.calledWith(
75+
expect(safeElement.setPrefixedAttribute).to.have.been.calledWith(
76+
[safeAttrPrefix`src`],
77+
el,
6978
'src',
7079
'http://localhost/url'
7180
);

packages/database/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,13 @@
5050
"license": "Apache-2.0",
5151
"peerDependencies": {},
5252
"dependencies": {
53-
"@firebase/logger": "0.4.2",
54-
"@firebase/util": "1.9.6",
55-
"@firebase/component": "0.6.7",
5653
"@firebase/app-check-interop-types": "0.3.2",
5754
"@firebase/auth-interop-types": "0.2.3",
55+
"@firebase/component": "0.6.7",
56+
"@firebase/logger": "0.4.2",
57+
"@firebase/util": "1.9.6",
5858
"faye-websocket": "0.11.4",
59+
"safevalues": "^0.5.2",
5960
"tslib": "^2.1.0"
6061
},
6162
"devDependencies": {

packages/database/src/realtime/BrowserPollConnection.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717

1818
import { base64Encode, isNodeSdk, stringify } from '@firebase/util';
19+
import { sanitizeHtml } from 'safevalues';
20+
import { safeDocument } from 'safevalues/dom';
1921

2022
import { RepoInfo, repoInfoConnectionURL } from '../core/RepoInfo';
2123
import { StatsCollection } from '../core/stats/StatsCollection';
@@ -475,7 +477,7 @@ export class FirebaseIFrameScriptHolder {
475477
const iframeContents = '<html><body>' + script + '</body></html>';
476478
try {
477479
this.myIFrame.doc.open();
478-
this.myIFrame.doc.write(iframeContents);
480+
safeDocument.write(this.myIFrame.doc, sanitizeHtml(iframeContents));
479481
this.myIFrame.doc.close();
480482
} catch (e) {
481483
log('frame writing exception');

packages/messaging/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,19 @@
5454
"@firebase/app": "0.x"
5555
},
5656
"dependencies": {
57+
"@firebase/component": "0.6.7",
5758
"@firebase/installations": "0.6.7",
5859
"@firebase/messaging-interop-types": "0.2.2",
5960
"@firebase/util": "1.9.6",
60-
"@firebase/component": "0.6.7",
6161
"idb": "7.1.1",
62+
"safevalues": "^0.5.2",
6263
"tslib": "^2.1.0"
6364
},
6465
"devDependencies": {
6566
"@firebase/app": "0.10.5",
67+
"@rollup/plugin-json": "4.1.0",
6668
"rollup": "2.79.1",
6769
"rollup-plugin-typescript2": "0.31.2",
68-
"@rollup/plugin-json": "4.1.0",
6970
"ts-essentials": "9.3.0",
7071
"typescript": "4.7.4"
7172
},

0 commit comments

Comments
 (0)