Skip to content

Commit

Permalink
Acquiring list of validDomains from CDN (#2089)
Browse files Browse the repository at this point in the history
* Adding in validDomains json object and publishing it as an Artifact

* Change files

* Renamed utils folder to artifactsForCDN and explicitly referencing validDomains.json file in build-test-publish

* Update @microsoft-teams-js-9f9754ac-4921-4f0b-a615-4498501e1ee1.json

* Renamed validOrigins to validDomains

* Updated reference from utils to artifactsForCDN

* Testing validDomains acquisition from CDN

* Moved validDomains.json into src folder and modified utils.ts to include reference to CDN

* Removed async calls

* Reverting back to main utils

* Added fetch mock for jest tests

* Change files

* Removed console.log statements

* Removing hardcoded list of validDomains

* Moved validOriginsJson to constants

* Added missing description for validOriginsFallback

* Added in validDomains.ts file, changed the way we call functions, looking into PrivateApi.spec for why unsupported domains are failing/timing out

* Updated tests to account for async processing of messages

* Updated tests and added new unit tests for fallback logic

* Added in 'caching' for validDomains

* Fixed improper import of json file

* Made JSON nomenclature more known

* Added logging, renamed validDomains to validOrigins for consistency, added JSON validation, repalced badactor with badactor.example in unit tests

* Added in SSR check

* Changed validateOriginsFromCDN to isValidOriginsJSONVald

* Fixed naming and try catch block

* Updated SDF endpoint to PROD endpoint

---------

Co-authored-by: Trevor Harris <[email protected]>
  • Loading branch information
jadahiya-MSFT and TrevorJoelHarris authored Jan 10, 2024
1 parent 41dd10e commit edef8cc
Show file tree
Hide file tree
Showing 42 changed files with 1,305 additions and 832 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Added new feature to acquire list of valid origins from a CDN endpoint",
"packageName": "@microsoft/teams-js",
"email": "[email protected]",
"dependentChangeType": "patch"
}
42 changes: 22 additions & 20 deletions packages/teams-js/src/internal/communication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { callHandler } from './handlers';
import { DOMMessageEvent, ExtendedWindow } from './interfaces';
import { MessageRequest, MessageRequestWithRequiredProperties, MessageResponse } from './messageObjects';
import { getLogger, isFollowingApiVersionTagFormat } from './telemetry';
import { ssrSafeWindow, validateOrigin } from './utils';
import { ssrSafeWindow } from './utils';
import { validateOrigin } from './validOrigins';

const communicationLogger = getLogger('communication');

Expand Down Expand Up @@ -65,7 +66,7 @@ export function initializeCommunication(
apiVersionTag: string,
): Promise<InitializeResponse> {
// Listen for messages post to our window
CommunicationPrivate.messageListener = (evt: DOMMessageEvent): void => processMessage(evt);
CommunicationPrivate.messageListener = async (evt: DOMMessageEvent): Promise<void> => await processMessage(evt);

// If we are in an iframe, our parent window is the one hosting us (i.e., window.parent); otherwise,
// it's the window that opened us (i.e., window.opener)
Expand Down Expand Up @@ -452,7 +453,7 @@ const processMessageLogger = communicationLogger.extend('processMessage');
* @internal
* Limited to Microsoft-internal use
*/
function processMessage(evt: DOMMessageEvent): void {
async function processMessage(evt: DOMMessageEvent): Promise<void> {
// Process only if we received a valid message
if (!evt || !evt.data || typeof evt.data !== 'object') {
processMessageLogger('Unrecognized message format received by app, message being ignored. Message: %o', evt);
Expand All @@ -464,22 +465,23 @@ function processMessage(evt: DOMMessageEvent): void {
// in their call to app.initialize
const messageSource = evt.source || (evt.originalEvent && evt.originalEvent.source);
const messageOrigin = evt.origin || (evt.originalEvent && evt.originalEvent.origin);
if (!shouldProcessMessage(messageSource, messageOrigin)) {
processMessageLogger(
'Message being ignored by app because it is either coming from the current window or a different window with an invalid origin',
);
return;
}

// Update our parent and child relationships based on this message
updateRelationships(messageSource, messageOrigin);

// Handle the message
if (messageSource === Communication.parentWindow) {
handleParentMessage(evt);
} else if (messageSource === Communication.childWindow) {
handleChildMessage(evt);
}
return shouldProcessMessage(messageSource, messageOrigin).then((result) => {
if (!result) {
processMessageLogger(
'Message being ignored by app because it is either coming from the current window or a different window with an invalid origin',
);
return;
}
// Update our parent and child relationships based on this message
updateRelationships(messageSource, messageOrigin);
// Handle the message
if (messageSource === Communication.parentWindow) {
handleParentMessage(evt);
} else if (messageSource === Communication.childWindow) {
handleChildMessage(evt);
}
});
}

const shouldProcessMessageLogger = communicationLogger.extend('shouldProcessMessage');
Expand All @@ -491,7 +493,7 @@ const shouldProcessMessageLogger = communicationLogger.extend('shouldProcessMess
* @internal
* Limited to Microsoft-internal use
*/
function shouldProcessMessage(messageSource: Window, messageOrigin: string): boolean {
async function shouldProcessMessage(messageSource: Window, messageOrigin: string): Promise<boolean> {
// Process if message source is a different window and if origin is either in
// Teams' pre-known whitelist or supplied as valid origin by user during initialization
if (Communication.currentWindow && messageSource === Communication.currentWindow) {
Expand All @@ -505,7 +507,7 @@ function shouldProcessMessage(messageSource: Window, messageOrigin: string): boo
) {
return true;
} else {
const isOriginValid = validateOrigin(new URL(messageOrigin));
const isOriginValid = await validateOrigin(new URL(messageOrigin));
if (!isOriginValid) {
shouldProcessMessageLogger('Message has an invalid origin of %s', messageOrigin);
}
Expand Down
70 changes: 28 additions & 42 deletions packages/teams-js/src/internal/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as validOriginsJSON from '../artifactsForCDN/validDomains.json';

/**
* @hidden
* The client version when all SDK APIs started to check platform compatibility for the APIs was 1.6.0.
Expand Down Expand Up @@ -110,48 +112,32 @@ export const scanBarCodeAPIMobileSupportVersion = '1.9.0';

/**
* @hidden
* List of supported Host origins
*
* @internal
* Limited to Microsoft-internal use
*/
export const validOrigins = [
'teams.microsoft.com',
'teams.microsoft.us',
'gov.teams.microsoft.us',
'dod.teams.microsoft.us',
'int.teams.microsoft.com',
'teams.live.com',
'devspaces.skype.com',
'ssauth.skype.com',
'local.teams.live.com', // local development
'local.teams.live.com:8080', // local development
'local.teams.office.com', // local development
'local.teams.office.com:8080', // local development
'outlook.office.com',
'outlook-sdf.office.com',
'outlook.office365.com',
'outlook-sdf.office365.com',
'outlook.live.com',
'outlook-sdf.live.com',
'*.teams.microsoft.com',
'*.www.office.com',
'www.office.com',
'word.office.com',
'excel.office.com',
'powerpoint.office.com',
'www.officeppe.com',
'*.www.microsoft365.com',
'www.microsoft365.com',
'bing.com',
'edgeservices.bing.com',
'www.bing.com',
'www.staging-bing-int.com',
'teams.cloud.microsoft',
'outlook.cloud.microsoft',
'm365.cloud.microsoft',
'copilot.microsoft.com',
];
* Fallback list of valid origins in JSON format
*
* @internal
* Limited to Microsoft-internal use
*/
const validOriginsLocal = validOriginsJSON;

/**
* @hidden
* Fallback list of valid origins
*
* @internal
* Limited to Microsoft-internal use
*/
export const validOriginsFallback = validOriginsLocal.validOrigins;

/**
* @hidden
* CDN endpoint of the list of valid origins
*
* @internal
* Limited to Microsoft-internal use
*/
export const validOriginsCdnEndpoint = new URL(
'https://res.cdn.office.net/teams-js/validDomains/json/validDomains.json',
);

/**
* @hidden
Expand Down
69 changes: 0 additions & 69 deletions packages/teams-js/src/internal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,78 +2,9 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import * as uuid from 'uuid';

import { GlobalVars } from '../internal/globalVars';
import { minAdaptiveCardVersion } from '../public/constants';
import { AdaptiveCardVersion, SdkError } from '../public/interfaces';
import { pages } from '../public/pages';
import { validOrigins } from './constants';
import { getLogger } from './telemetry';

/**
* @param pattern - reference pattern
* @param host - candidate string
* @returns returns true if host matches pre-know valid pattern
*
* @example
* validateHostAgainstPattern('*.teams.microsoft.com', 'subdomain.teams.microsoft.com') returns true
* validateHostAgainstPattern('teams.microsoft.com', 'team.microsoft.com') returns false
*
* @internal
* Limited to Microsoft-internal use
*/
function validateHostAgainstPattern(pattern: string, host: string): boolean {
if (pattern.substring(0, 2) === '*.') {
const suffix = pattern.substring(1);
if (
host.length > suffix.length &&
host.split('.').length === suffix.split('.').length &&
host.substring(host.length - suffix.length) === suffix
) {
return true;
}
} else if (pattern === host) {
return true;
}
return false;
}

const validateOriginLogger = getLogger('validateOrigin');

/**
* @internal
* Limited to Microsoft-internal use
*/
export function validateOrigin(messageOrigin: URL): boolean {
// Check whether the url is in the pre-known allowlist or supplied by user
if (!isValidHttpsURL(messageOrigin)) {
validateOriginLogger(
'Origin %s is invalid because it is not using https protocol. Protocol being used: %s',
messageOrigin,
messageOrigin.protocol,
);
return false;
}
const messageOriginHost = messageOrigin.host;

if (validOrigins.some((pattern) => validateHostAgainstPattern(pattern, messageOriginHost))) {
return true;
}

for (const domainOrPattern of GlobalVars.additionalValidOrigins) {
const pattern = domainOrPattern.substring(0, 8) === 'https://' ? domainOrPattern.substring(8) : domainOrPattern;
if (validateHostAgainstPattern(pattern, messageOriginHost)) {
return true;
}
}

validateOriginLogger(
'Origin %s is invalid because it is not an origin approved by this library or included in the call to app.initialize.\nOrigins approved by this library: %o\nOrigins included in app.initialize: %o',
messageOrigin,
validOrigins,
GlobalVars.additionalValidOrigins,
);
return false;
}

/**
* @internal
Expand Down
131 changes: 131 additions & 0 deletions packages/teams-js/src/internal/validOrigins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { validOriginsCdnEndpoint, validOriginsFallback } from './constants';
import { GlobalVars } from './globalVars';
import { getLogger } from './telemetry';
import { inServerSideRenderingEnvironment, isValidHttpsURL } from './utils';

let validOriginsCache: string[] = [];
const validateOriginLogger = getLogger('validateOrigin');

export async function prefetchOriginsFromCDN(): Promise<void> {
await getValidOriginsListFromCDN();
}

function isValidOriginsCacheEmpty(): boolean {
return validOriginsCache.length !== 0;
}

async function getValidOriginsListFromCDN(): Promise<string[]> {
if (isValidOriginsCacheEmpty()) {
return validOriginsCache;
}
if (!inServerSideRenderingEnvironment()) {
return fetch(validOriginsCdnEndpoint)
.then((response) => {
if (!response.ok) {
throw new Error('Invalid Response from Fetch Call');
}
return response.json().then((validOriginsCDN) => {
if (isValidOriginsJSONValid(JSON.stringify(validOriginsCDN))) {
validOriginsCache = validOriginsCDN.validOrigins;
return validOriginsCache;
} else {
throw new Error('Valid Origins List Is Invalid');
}
});
})
.catch((e) => {
validateOriginLogger('validOrigins fetch call to CDN failed with error: %s. Defaulting to fallback list', e);
validOriginsCache = validOriginsFallback;
return validOriginsCache;
});
} else {
validOriginsCache = validOriginsFallback;
return validOriginsFallback;
}
}

function isValidOriginsJSONValid(validOriginsJSON: string): boolean {
let validOriginsCDN = JSON.parse(validOriginsJSON);
try {
validOriginsCDN = JSON.parse(validOriginsJSON);
} catch (_) {
return false;
}
if (!validOriginsCDN.validOrigins) {
return false;
}
for (const validOrigin of validOriginsCDN.validOrigins) {
try {
new URL('https://' + validOrigin);
} catch (_) {
validateOriginLogger('isValidOriginsFromCDN call failed to validate origin: %s', validOrigin);
return false;
}
}
return true;
}

/**
* @param pattern - reference pattern
* @param host - candidate string
* @returns returns true if host matches pre-know valid pattern
*
* @example
* validateHostAgainstPattern('*.teams.microsoft.com', 'subdomain.teams.microsoft.com') returns true
* validateHostAgainstPattern('teams.microsoft.com', 'team.microsoft.com') returns false
*
* @internal
* Limited to Microsoft-internal use
*/
function validateHostAgainstPattern(pattern: string, host: string): boolean {
if (pattern.substring(0, 2) === '*.') {
const suffix = pattern.substring(1);
if (
host.length > suffix.length &&
host.split('.').length === suffix.split('.').length &&
host.substring(host.length - suffix.length) === suffix
) {
return true;
}
} else if (pattern === host) {
return true;
}
return false;
}

/**
* @internal
* Limited to Microsoft-internal use
*/
export function validateOrigin(messageOrigin: URL): Promise<boolean> {
return getValidOriginsListFromCDN().then((validOriginsList) => {
// Check whether the url is in the pre-known allowlist or supplied by user
if (!isValidHttpsURL(messageOrigin)) {
validateOriginLogger(
'Origin %s is invalid because it is not using https protocol. Protocol being used: %s',
messageOrigin,
messageOrigin.protocol,
);
return false;
}
const messageOriginHost = messageOrigin.host;
if (validOriginsList.some((pattern) => validateHostAgainstPattern(pattern, messageOriginHost))) {
return true;
}

for (const domainOrPattern of GlobalVars.additionalValidOrigins) {
const pattern = domainOrPattern.substring(0, 8) === 'https://' ? domainOrPattern.substring(8) : domainOrPattern;
if (validateHostAgainstPattern(pattern, messageOriginHost)) {
return true;
}
}

validateOriginLogger(
'Origin %s is invalid because it is not an origin approved by this library or included in the call to app.initialize.\nOrigins approved by this library: %o\nOrigins included in app.initialize: %o',
messageOrigin,
validOriginsList,
GlobalVars.additionalValidOrigins,
);
return false;
});
}
Loading

0 comments on commit edef8cc

Please sign in to comment.