Skip to content

Commit d0af4f3

Browse files
May HartovMay Hartov
authored andcommitted
Merged PR 483438: JS SDK - embed URL validation
Following MSRC case as malicious site can be injected as the embed iframe src, added embed URL validation to ensure the host is an allowed PBI src. A valid embed url protocol is "https:" The valid hosts names are ([retrieved from EV2-deployment repository - all of ida_PowerBIFeUrl key values](https://dev.azure.com/powerbi/PowerBIClients/_search?action=contents&text=ida_PowerBIFeUrl path%3A*envParams*&type=code&lp=code-Project&filters=ProjectFilters{PowerBIClients}RepositoryFilters{PowerBIClients-EV2-Deployment}&pageSize=25&result=DefaultCollection/PowerBIClients/PowerBIClients-EV2-Deployment/GBmaster//WFE/AppService/ADM/Public/INT/envParams.txt)): - app.powerbi.com, - app.powerbi.cn, - app.powerbigov.us, - app.mil.powerbigov.us, - app.high.powerbigov.us, - app.powerbi.eaglex.ic.gov, - app.powerbi.microsoft.scloud, - powerbi-df.analysis-df.windows.net, - CST WFE URLs: 'https://{cst-name}.analysis.windows-int.net' - daily.powerbi.com - dxt.powerbi.com - msit.powerbi.com Embed URL validation should include fabric embed URL. All of the above should be covered by the following regex expressions: .+\.powerbi.com$ - daily.powerbi.com - dxt.powerbi.com - msit.powerbi.com - app.powerbi.com FF: ^app(.mil.|.high.|.)powerbigov.us$ - app.powerbigov.us, - app.mil.powerbigov.us, - app.high.powerbigov.us Edog: .+\.analysis-df.windows.net$ Onebox and CSTs: .+\.analysis.windows-int.net$ Fabric URLs: .+\.fabric.microsoft.com$ **Please look into the test cases in utils.spec.ts to see the valid and invalid embe urls** Related work items: #1245653
1 parent 61e39a9 commit d0af4f3

13 files changed

+172
-263
lines changed

dist/powerbi-client.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ declare module "config" {
1212
declare module "errors" {
1313
export const APINotSupportedForRDLError = "This API is currently not supported for RDL reports";
1414
export const EmbedUrlNotSupported = "Embed URL is invalid for this scenario. Please use Power BI REST APIs to get the valid URL";
15+
export const invalidEmbedUrlErrorMessage: string;
1516
}
1617
declare module "util" {
1718
import { HttpPostMessage } from 'http-post-message';
@@ -124,6 +125,11 @@ declare module "util" {
124125
* @returns {boolean}
125126
*/
126127
export function isCreate(embedType: string): boolean;
128+
/**
129+
* Checks if the embedUrl has an allowed power BI domain
130+
* @hidden
131+
*/
132+
export function validateEmbedUrl(embedUrl: string): boolean;
127133
}
128134
declare module "embed" {
129135
import * as models from 'powerbi-models';
@@ -909,7 +915,7 @@ declare module "page" {
909915
* Get insights for report page
910916
*
911917
* ```javascript
912-
* page.getSmartNarrativeInsights()
918+
* page.getSmartNarrativeInsights();
913919
* ```
914920
*
915921
* @returns {Promise<ISmartNarratives>}

dist/powerbi.js

Lines changed: 57 additions & 253 deletions
Large diffs are not rendered by default.

dist/powerbi.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/embed.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
import * as models from 'powerbi-models';
55
import * as sdkConfig from './config';
6-
import { EmbedUrlNotSupported } from './errors';
6+
import { EmbedUrlNotSupported, invalidEmbedUrlErrorMessage } from './errors';
77
import { ICustomEvent, IEvent, IEventHandler, Service } from './service';
8-
import { addParamToUrl, assign, autoAuthInEmbedUrl, createRandomString, getTimeDiffInMilliseconds, remove, isCreate } from './util';
8+
import { addParamToUrl, assign, autoAuthInEmbedUrl, createRandomString, getTimeDiffInMilliseconds, remove, isCreate, validateEmbedUrl } from './util';
99

1010
declare global {
1111
interface Document {
@@ -573,7 +573,7 @@ export abstract class Embed {
573573

574574
const accessTokenProvider = eventHooks.accessTokenProvider;
575575
if (!!accessTokenProvider) {
576-
if ((['create', 'quickcreate', 'report'].indexOf(this.embedtype.toLowerCase()) === -1) || this.config.tokenType !== models.TokenType.Aad) {
576+
if ((['create', 'quickcreate', 'report'].indexOf(this.embedtype.toLowerCase()) === -1) || this.config.tokenType !== models.TokenType.Aad) {
577577
throw new Error("accessTokenProvider is only supported in report SaaS embed");
578578
}
579579
}
@@ -634,10 +634,6 @@ export abstract class Embed {
634634
// Trim spaces to fix user mistakes.
635635
hostname = hostname.toLowerCase().trim();
636636

637-
if (hostname.indexOf("http://") === 0) {
638-
throw new Error("HTTP is not allowed. HTTPS is required");
639-
}
640-
641637
if (hostname.indexOf("https://") === 0) {
642638
return `${hostname}/${endpoint}`;
643639
}
@@ -745,6 +741,9 @@ export abstract class Embed {
745741
if (!this.iframe) {
746742
const iframeContent = document.createElement("iframe");
747743
const embedUrl = this.config.uniqueId ? addParamToUrl(this.config.embedUrl, 'uid', this.config.uniqueId) : this.config.embedUrl;
744+
if (!validateEmbedUrl(embedUrl)) {
745+
throw new Error(invalidEmbedUrlErrorMessage);
746+
}
748747

749748
iframeContent.style.width = '100%';
750749
iframeContent.style.height = '100%';

src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33

44
export const APINotSupportedForRDLError = "This API is currently not supported for RDL reports";
55
export const EmbedUrlNotSupported = "Embed URL is invalid for this scenario. Please use Power BI REST APIs to get the valid URL";
6+
export const invalidEmbedUrlErrorMessage: string = "Invalid embed URL detected. Either URL hostname or protocol are invalid. Please use Power BI REST APIs to get the valid URL";
67

src/service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { Visual } from './visual';
2929
import * as utils from './util';
3030
import { QuickCreate } from './quickCreate';
3131
import * as sdkConfig from './config';
32+
import { invalidEmbedUrlErrorMessage } from './errors';
3233

3334
export interface IEvent<T> {
3435
type: string;
@@ -667,6 +668,10 @@ export class Service implements IService {
667668
* @param {HTMLElement} [element=undefined]
668669
*/
669670
preload(config: IComponentEmbedConfiguration | IEmbedConfigurationBase, element?: HTMLElement): HTMLIFrameElement {
671+
if (!utils.validateEmbedUrl(config.embedUrl)) {
672+
throw new Error(invalidEmbedUrlErrorMessage);
673+
}
674+
670675
const iframeContent = document.createElement("iframe");
671676
iframeContent.setAttribute("style", "display:none;");
672677
iframeContent.setAttribute("src", config.embedUrl);

src/util.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@
33

44
import { HttpPostMessage } from 'http-post-message';
55

6+
/**
7+
* @hidden
8+
*/
9+
const allowedPowerBiHostsRegex =
10+
new RegExp(/(.+\.powerbi\.com$)|(.+\.fabric\.microsoft\.com$)|(.+\.analysis\.windows-int\.net$)|(.+\.analysis-df\.windows\.net$)/g);
11+
12+
/**
13+
* @hidden
14+
*/
15+
const allowedPowerBiHostsSovRegex = new RegExp(/^app\.powerbi\.cn$|^app(\.mil\.|\.high\.|\.)powerbigov\.us$|^app\.powerbi\.eaglex\.ic\.gov$|^app\.powerbi\.microsoft\.scloud$/g);
16+
17+
/**
18+
* @hidden
19+
*/
20+
const expectedEmbedUrlProtocol: string = "https:";
21+
622
/**
723
* Raises a custom event with event data on the specified HTML element.
824
*
@@ -223,3 +239,21 @@ export function getTimeDiffInMilliseconds(start: Date, end: Date): number {
223239
export function isCreate(embedType: string): boolean {
224240
return embedType === 'create' || embedType === 'quickcreate';
225241
}
242+
243+
/**
244+
* Checks if the embedUrl has an allowed power BI domain
245+
* @hidden
246+
*/
247+
export function validateEmbedUrl(embedUrl: string): boolean {
248+
if (embedUrl) {
249+
let url: URL;
250+
try {
251+
url = new URL(embedUrl.toLowerCase());
252+
} catch(e) {
253+
// invalid URL
254+
return false;
255+
}
256+
return url.protocol === expectedEmbedUrlProtocol &&
257+
(allowedPowerBiHostsRegex.test(url.hostname) || allowedPowerBiHostsSovRegex.test(url.hostname));
258+
}
259+
}

test/SDK-to-HPM.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ describe('SDK-to-HPM', function () {
4848
};
4949

5050
spyOn(utils, "getTimeDiffInMilliseconds").and.callFake(() => 700); // Prevent requests from being throttled.
51+
spyOn(utils, 'validateEmbedUrl').and.callFake(() => { return true; });
5152

5253
powerbi = new service.Service(spyHpmFactory, noop, spyRouterFactory, { wpmpName: 'SDK-to-HPM report wpmp' });
53-
54+
5455
sdkSessionId = powerbi.getSdkSessionId();
5556
});
5657

test/SDK-to-MockApp.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ describe('SDK-to-MockApp', function () {
3131
powerbi = new service.Service(factories.hpmFactory, factories.wpmpFactory, factories.routerFactory, {
3232
wpmpName: 'SDK-to-MockApp HostWpmp'
3333
});
34+
35+
spyOn(utils, 'validateEmbedUrl').and.callFake(() => { return true; });
36+
3437
element = document.createElement('div');
3538
element.id = "reportContainer1";
3639
element.className = 'powerbi-report-container2';

test/SDK-to-WPMP.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as service from '../src/service';
55
import * as report from '../src/report';
66
import * as Wpmp from 'window-post-message-proxy';
77
import * as factories from '../src/factories';
8+
import * as utils from '../src/util';
89
import { spyWpmp } from './utility/mockWpmp';
910
import { spyHpm } from './utility/mockHpm';
1011
import { spyRouter } from './utility/mockRouter';
@@ -17,6 +18,7 @@ describe('SDK-to-WPMP', function () {
1718
let uniqueId: string;
1819

1920
beforeEach(function () {
21+
spyOn(utils, 'validateEmbedUrl').and.callFake(() => { return true; });
2022
const spyWpmpFactory: factories.IWpmpFactory = (_name?: string, _logMessages?: boolean) => {
2123
return <Wpmp.WindowPostMessageProxy>spyWpmp;
2224
};

0 commit comments

Comments
 (0)