Skip to content

Commit

Permalink
[main] Custom Trusted Type Policy Support for Snippet Script Injection (
Browse files Browse the repository at this point in the history
  • Loading branch information
siyuniu-ms authored Sep 10, 2024
1 parent 5df231c commit 5363b35
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 1 deletion.
4 changes: 4 additions & 0 deletions tools/applicationinsights-web-snippet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ let config = {connectionString: "InstrumentationKey=xxx", sri: true};

More details on web snippet, see [Web Snippet](https://github.com/microsoft/ApplicationInsights-JS#snippet-setup-ignore-if-using-npm-setup).

### Trusted Types Support (available since v1.2.2)
For restrictions like require-trusted-types-for 'script', check [url policy check](./trustedTypeSupport.md)
For restrictions like script-src 'self' ..., check [add nounce when inject script]()

## Build
```
npm install -g grunt-cli
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<meta http-equiv="Content-Security-Policy" content="img-src https://browser.events.data.microsoft.com;frame-src https://browser.events.data.microsoft.com;require-trusted-types-for 'script'">
<html>
<head>
<style>
/* Optional styling for buttons and messages */
button {
margin: 10px;
padding: 10px 20px;
font-size: 16px;
}
#messages {
margin: 20px;
padding: 10px;
border: 1px solid #ccc;
background-color: #f9f9f9;
}
</style>
<script type="text/javascript">
const myTrustedTypePolicy = trustedTypes.createPolicy('myTrustedTypePolicy', {
createScriptURL: (url) => {
console.log('Trusted Type Policy: myTrustedTypePolicy called with URL:', url);
return url;
}
});
!(function (cfg){function e(){cfg.onInit&&cfg.onInit(n)}var x,D,E,t,L,C,n,U=window,O=document,b=U.location,I="script",j="ingestionendpoint",q="disableExceptionTracking",A="ai.device.";"instrumentationKey"[x="toLowerCase"](),D="crossOrigin",E="POST",t="appInsightsSDK",L=cfg.name||"appInsights",C=cfg.pn||"aiPolicy",(cfg.name||U[t])&&(U[t]=L),n=U[L]||function(u){var s=!1,p=!1,l={initialize:!0,queue:[],sv:"8",version:2,config:u};function d(e){var t,n,i,a,r,o,c,s;!0!==cfg.dle&&(o=(t=function(){var e,t={},n=u.connectionString;if("string"==typeof n&&n)for(var i=n.split(";"),a=0;a<i.length;a++){var r=i[a].split("=");2===r.length&&(t[r[0][x]()]=r[1])}return t[j]||(e=(n=t.endpointsuffix)?t.location:null,t[j]="https://"+(e?e+".":"")+"dc."+(n||"services.visualstudio.com")),t}()).instrumentationkey||u.instrumentationKey||"",t=(t=(t=t[j])&&"/"===t.slice(-1)?t.slice(0,-1):t)?t+"/v2/track":u.endpointUrl,t=u.userOverrideEndpointUrl||t,(n=[]).push((i="SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details)",a=e,c=t,(s=(r=f(o,"Exception")).data).baseType="ExceptionData",s.baseData.exceptions=[{typeName:"SDKLoadFailed",message:i.replace(/\./g,"-"),hasFullStack:!1,stack:i+"\nSnippet failed to load ["+a+"] -- Telemetry is disabled\nHelp Link: https://go.microsoft.com/fwlink/?linkid=2128109\nHost: "+(b&&b.pathname||"_unknown_")+"\nEndpoint: "+c,parsedStack:[]}],r)),n.push((s=e,i=t,(c=(a=f(o,"Message")).data).baseType="MessageData",(r=c.baseData).message='AI (Internal): 99 message:"'+("SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details) ("+s+")").replace(/\"/g,"")+'"',r.properties={endpoint:i},a)),e=n,o=t,JSON&&((c=U.fetch)&&!cfg.useXhr?c(o,{method:E,body:JSON.stringify(e),mode:"cors"}):XMLHttpRequest&&((s=new XMLHttpRequest).open(E,o),s.setRequestHeader("Content-type","application/json"),s.send(JSON.stringify(e)))))}function f(e,t){var n={},i="Browser";function a(e){e=""+e;return 1===e.length?"0"+e:e}return n[A+"id"]=i[x](),n[A+"type"]=i,n["ai.operation.name"]=b&&b.pathname||"_unknown_",n["ai.internal.sdkVersion"]="javascript:snippet_"+(l.sv||l.version),{time:(i=new Date).getUTCFullYear()+"-"+a(1+i.getUTCMonth())+"-"+a(i.getUTCDate())+"T"+a(i.getUTCHours())+":"+a(i.getUTCMinutes())+":"+a(i.getUTCSeconds())+"."+(i.getUTCMilliseconds()/1e3).toFixed(3).slice(2,5)+"Z",iKey:e,name:"Microsoft.ApplicationInsights."+e.replace(/-/g,"")+"."+t,sampleRate:100,tags:n,data:{baseData:{ver:2}},ver:undefined,seq:"1",aiDataContract:undefined}}var n,i,t,a,g=-1,h=0,m=["js.monitor.azure.com","js.cdn.applicationinsights.io","js.cdn.monitor.azure.com","js0.cdn.applicationinsights.io","js0.cdn.monitor.azure.com","js2.cdn.applicationinsights.io","js2.cdn.monitor.azure.com","az416426.vo.msecnd.net"],r=u.url||cfg.src,o=function(){return c(r,null)};function c(t,r){if((n=navigator)&&(~(n=(n.userAgent||"").toLowerCase()).indexOf("msie")||~n.indexOf("trident/"))&&~t.indexOf("ai.3")&&(t=t.replace(/(\/)(ai\.3\.)([^\d]*)$/,function(e,t,n){return t+"ai.2"+n})),!1!==cfg.cr)for(var e=0;e<m.length;e++)if(0<t.indexOf(m[e])){g=e;break}var n,o=function(e){var a;l.queue=[],p||(0<=g&&h+1<m.length?(a=(g+h+1)%m.length,i(t.replace(/^(.*\/\/)([\w\.]*)(\/.*)$/,function(e,t,n,i){return t+m[a]+i})),h+=1):(s=p=!0,d(t)))},c=function(e,t){p||setTimeout(function(){!t&&l.core||o()},500),s=!1},i=function(e){var n,i=O.createElement(I),e=(cfg.pl?cfg.ttp&&cfg.ttp.createScript?i.src=cfg.ttp.createScriptURL(e):i.src=(null==(n=window.trustedTypes)?void 0:n.createPolicy(C,{createScriptURL:function(e){try{var t=new URL(e);if(t.host&&"js.monitor.azure.com"===t.host)return e;a(e)}catch(n){a(e)}}})).createScriptURL(e):i.src=e,r&&(i.integrity=r),i.setAttribute("data-ai-name",L),cfg[D]);function a(e){d("AI policy blocked URL: "+e)}return!e&&""!==e||"undefined"==i[D]||(i[D]=e),i.onload=c,i.onerror=o,i.onreadystatechange=function(e,t){"loaded"!==i.readyState&&"complete"!==i.readyState||c(0,t)},cfg.ld&&cfg.ld<0?O.getElementsByTagName("head")[0].appendChild(i):setTimeout(function(){O.getElementsByTagName(I)[0].parentNode.appendChild(i)},cfg.ld||0),i};i(t)}cfg.sri&&(n=r.match(/^((http[s]?:\/\/.*\/)\w+(\.\d+){1,5})\.(([\w]+\.){0,2}js)$/))&&6===n.length?(T="".concat(n[1],".integrity.json"),i="@".concat(n[4]),S=window.fetch,t=function(e){if(!e.ext||!e.ext[i]||!e.ext[i].file)throw Error("Error Loading JSON response");var t=e.ext[i].integrity||null;c(r=n[2]+e.ext[i].file,t)},S&&!cfg.useXhr?S(T,{method:"GET",mode:"cors"}).then(function(e){return e.json()["catch"](function(){return{}})}).then(t)["catch"](o):XMLHttpRequest&&((a=new XMLHttpRequest).open("GET",T),a.onreadystatechange=function(){if(a.readyState===XMLHttpRequest.DONE)if(200===a.status)try{t(JSON.parse(a.responseText))}catch(e){o()}else o()},a.send())):r&&o();try{l.cookie=O.cookie}catch(k){}function e(e){for(;e.length;)!function(t){l[t]=function(){var e=arguments;s||l.queue.push(function(){l[t].apply(l,e)})}}(e.pop())}var v,y,S="track",T="TrackPage",w="TrackEvent",S=(e([S+"Event",S+"PageView",S+"Exception",S+"Trace",S+"DependencyData",S+"Metric",S+"PageViewPerformance","start"+T,"stop"+T,"start"+w,"stop"+w,"addTelemetryInitializer","setAuthenticatedUserContext","clearAuthenticatedUserContext","flush"]),l.SeverityLevel={Verbose:0,Information:1,Warning:2,Error:3,Critical:4},(u.extensionConfig||{}).ApplicationInsightsAnalytics||{});return!0!==u[q]&&!0!==S[q]&&(e(["_"+(v="onerror")]),y=U[v],U[v]=function(e,t,n,i,a){var r=y&&y(e,t,n,i,a);return!0!==r&&l["_"+v]({message:e,url:t,lineNumber:n,columnNumber:i,error:a,evt:U.event}),r},u.autoExceptionInstrumented=!0),l}(cfg.cfg),(U[L]=n).queue&&0===n.queue.length?(n.queue.push(e),n.trackPageView({})):e();})({
src: "https://js.monitor.azure.com/scripts/b/ai.3.gbl.min.js",
// name: "appInsights", // Global SDK Instance name defaults to "appInsights" when not supplied
// ld: 0, // Defines the load delay (in ms) before attempting to load the sdk. -1 = block page load and add to head. (default) = 0ms load after timeout,
// useXhr: 1, // Use XHR instead of fetch to report failures (if available),
// dle: true, // Prevent the SDK from reporting load failure log
crossOrigin: "anonymous", // When supplied this will add the provided value as the cross origin attribute on the script tag
// onInit: null, // Once the application insights instance has loaded and initialized this callback function will be called with 1 argument -- the sdk instance (DO NOT ADD anything to the sdk.queue -- As they won't get called)
// sri: false, // Custom optional value to specify whether fetching the snippet from integrity file and do integrity check
pl: true, // Optional value to specify whether to use Trusted Types for script URL
pn: "aiPolicy", // Optional value to specify the policy name for Trusted Types
ttp: myTrustedTypePolicy,
cfg: { // Application Insights Configuration
connectionString: "InstrumentationKey=814a172a-92fd-4950-9023-9cf13bb65696;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/"
}
});
</script>
</head>
<body>
<div id="messages"></div>
<button id="myButton">Send Telemetry</button>

<script nonce="randomNonceValue" >
function sendTelemetry() {
appInsights.trackEvent({name: 'test event'});
appInsights.flush();
}
</script>
<script nonce="randomNonceValue">
document.getElementById('myButton').addEventListener('click', sendTelemetry);
</script>

<script nonce="1234">

function displayMessage(message, type) {
var messageDiv = document.createElement('div');
messageDiv.className = type;
messageDiv.textContent = message;
document.getElementById('messages').appendChild(messageDiv);
}


</script>
<script>
var windowOnError = window.onerror;
window.onerror = function(message, source, lineno, colno, error) {
windowOnError && windowOnError(message, source, lineno, colno, error);
console.log('Error captured:', message, source, lineno, colno, error);

// Display the error message on the page
var errorMessage = document.createElement('div');
errorMessage.className = 'error';
errorMessage.textContent = 'Error: ' + message + ' at ' + source + ':' + lineno + ':' + colno;
document.getElementById('messages').appendChild(errorMessage);
};
</script>

</body>
</html>
38 changes: 37 additions & 1 deletion tools/applicationinsights-web-snippet/src/snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ declare var cfg:ISnippetConfig;
let strGetMethod = "GET";
let sdkInstanceName = "appInsightsSDK"; // required for Initialization to find the current instance
let aiName = cfg.name || "appInsights"; // provide non default instance name through snipConfig name value
let policyName = cfg.pn || "aiPolicy";
if (cfg.name || win[sdkInstanceName]) {
// Only set if supplied or another name is defined to avoid polluting the global namespace
win[sdkInstanceName] = aiName;
Expand Down Expand Up @@ -304,9 +305,44 @@ declare var cfg:ISnippetConfig;
loadFailed = false;
}

function create_policy() {
// Function to handle URL validation
function validateURL(urlString: string): string | null {
try {
const url = new URL(urlString);
if (url.host && url.host === "js.monitor.azure.com") {
return urlString;
} else {
handleInvalidURL(urlString);
}
} catch {
handleInvalidURL(urlString);
}
}

// Function to handle reporting failures
function handleInvalidURL(urlString: string) {
_reportFailure("AI policy blocked URL: " + urlString);
}

return (window as any).trustedTypes?.createPolicy(policyName, {
createScriptURL: validateURL
});
}


const _createScript = (src: string) => {
let scriptElement : HTMLElement = doc.createElement(scriptText);
(scriptElement as any)["src"] = src;
if (cfg.pl){
if (cfg.ttp && cfg.ttp.createScript) {
(scriptElement as any)["src"] = cfg.ttp.createScriptURL(src);
} else {
(scriptElement as any)["src"] = create_policy().createScriptURL(src);
}
} else {
(scriptElement as any)["src"] = src;
}

if (integrity){
// Set the integrity attribute to the script tag if integrity is provided
(scriptElement as any).integrity = integrity;
Expand Down
22 changes: 22 additions & 0 deletions tools/applicationinsights-web-snippet/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ export interface SdkLoaderConfig {
cr?: boolean;
dle?: boolean;
sri?: boolean;
pl?: boolean;
pn?: string;
ttp?: TrustedTypePolicy;
}

export abstract class TrustedTypePolicy {
readonly name: string;
createHTML?: ((input: string, ...args: any[]) => string) | undefined;
createScript?: ((input: string, ...args: any[]) => string) | undefined;
createScriptURL?: ((input: string, ...args: any[]) => string) | undefined;
}

export interface ISnippetConfig {
Expand All @@ -25,6 +35,18 @@ export interface ISnippetConfig {
cr?: boolean; // cdn retry would be proceed if ture
dle?: boolean; // Custom optional value to disable sdk load error to be sent
sri?: boolean; // Custom optional value to specify whether fetching the snippet from integrity file and do integrity check
/**
* Custom optional value to specify whether to enable the trusted type policy check on snippet
*/
pl?: boolean;
/**
* Custom optional value to specify the name of the trusted type policy that would be implemented on the snippet, default is 'aiPolicy'
*/
pn?: string;
/*
* Custom optional value to specify the trusted type policy that would be applied on the snippet src
*/
ttp?: TrustedTypePolicy;
}

export interface Fields {
Expand Down
62 changes: 62 additions & 0 deletions tools/applicationinsights-web-snippet/trustedTypeSupport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Trust Type Support

We offer two methods for implementing Trusted Type policy checks. Choose the one that best suits your needs.

## Method 1: Using require-trusted-types-for 'script'
If your page utilizes require-trusted-types-for 'script' to enforce script injection policies, configure your snippet as follows:
### Configuration Options
```js
/**
* Custom optional value to specify whether to enable the trusted type policy check on snippet
*/
pl?: boolean;
/**
* Custom optional value to specify the name of the trusted type policy that would be implemented on the snippet, default is 'aiPolicy'
*/
pn?: string;
/*
* Custom optional value to specify the trusted type policy that would be applied on the snippet src
*/
ttp?: TrustedTypePolicy;
```
### Automatic Policy Creation
To have the policy automatically created, set pl to true. You can optionally specify a policy name with pn.
Example usage:
```html
<script>
!(function (cfg) ....)({
src: "https://js.monitor.azure.com/scripts/b/ai.3.gbl.min.js",
pl: true,
pn: "aiPolicy",
cfg: {
connectionString: ""
}
});
</script>
```
### Using a Custom Trusted Type Policy
If you prefer to pass your own Trusted Type Policy, create it and then apply it using the ttp option.

Example:
```html
<script>
const myTrustedTypePolicy = trustedTypes.createPolicy('myTrustedTypePolicy', {
createScriptURL: (url) => {
console.log('Trusted Type Policy: myTrustedTypePolicy called with URL:', url);
return url;
}
});
!(function (cfg) ....)({
src: "https://js.monitor.azure.com/scripts/b/ai.3.gbl.min.js",
pl: true,
ttp: myTrustedTypePolicy,
cfg: {
connectionString: ""
}
});
</script>
```
### Test
Your could also check our [test](./Tests/manual/cspUsePolicyTest.html)

## Method 2: Using Nonce Tag and script-src

0 comments on commit 5363b35

Please sign in to comment.