Skip to content

Security: Phobetore/ShadowLink

Security

SECURITY.md

Security policy

Reporting something

Report privately, through GitHub's private advisory form. It reaches the maintainer without the report being public first. Please do not open a normal issue for a vulnerability.

Tell me what you found, how to reproduce it, and what an attacker gets out of it. A working proof of concept is welcome but not required.

ShadowLink is maintained by one person. Expect an acknowledgement within a week, and a fix released when it is written and tested rather than on a schedule I would end up missing. You will be credited unless you would rather not be.

Which versions get fixes

The newest release, and nothing else. The project is young enough that there is no sensible support window to promise.

What the threat model actually is

Read this before deciding where to run a server. These are properties of the current release, not oversights I am unaware of.

One key guards the whole server.SERVER_KEY is checked at the WebSocket upgrade, before any document data flows. That is the only authentication there is. Anyone holding it can join any workspace on that server, read everything in it, and write to it. There are no accounts, no per-member permissions, no read-only members, and no invitations. Treat the key exactly like a shared password: send it through a password manager or a private channel, never a public repository or a shared document.

Rotating the key locks everyone out. Delete the server's data/tokens.json and restart — the server mints a new key, and every member has to be given it. There is no way to revoke one member.

The server is trusted. Documents are stored and relayed in plaintext. Anyone with access to the server process or its PERSISTENCE_DIR can read every shared note. End-to-end encryption is planned, and the storage layer was designed to keep it possible, but it does not exist yet. Do not put anything on a server you do not control that you would not hand to whoever controls it.

Transport is unencrypted by default. The server speaks plain ws://, which is fine inside a home network or a VPN. Across the open internet, put a reverse proxy in front of it (Caddy, Nginx or Traefik) so members connect over wss://. Without that, the key and every keystroke cross the network in the clear.

A member is trusted once they are in. There is no server-side validation of what a client writes into the shared documents. A hostile member can tombstone everything in a workspace, or fill it with rename churn. Client-side limits contain the blast radius — every path is validated before it touches a filesystem, nothing outside the shared folder can be written, nothing is ever hard-deleted, and a bulk deletion has to be confirmed by each recipient — but the shared document itself has no guard. Only share a key with people you would give write access to a shared drive.

Data loss is bounded on purpose. This is the part the design spends most of its effort on. Removal is never destructive: a deletion is a tombstone, your copy goes to Obsidian's own trash only when the content is provably in the shared document, and otherwise into ShadowLink Recovered/. More than ten deletions in ten minutes stops and asks, defaulting to keeping your files. A file the plugin cannot prove is safe is never destroyed.

What is in scope

  • Anything that lets a client read or write a workspace without the server key.
  • Any path a remote peer can use to write outside the configured shared folder, or to make the plugin delete something without the documented confirmation.
  • Any way to make the plugin destroy content irrecoverably — that is the project's central promise, and a break in it is the most serious report you can send.
  • Anything that leaks the server key or a note's content to a third party.

What is out of scope

  • The absence of end-to-end encryption, of per-member permissions, and of server-side validation. Those are documented above and on the roadmap.
  • Denial of service against a server you were given the key to.
  • Anything requiring an attacker who already has the server key, unless it crosses a boundary the key is not supposed to open.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Overview · Phobetore/ShadowLink · GitHub
Skip to content

Security: Phobetore/ShadowLink

Security

SECURITY.md

Security policy

Reporting something

Report privately, through GitHub's private advisory form. It reaches the maintainer without the report being public first. Please do not open a normal issue for a vulnerability.

Tell me what you found, how to reproduce it, and what an attacker gets out of it. A working proof of concept is welcome but not required.

ShadowLink is maintained by one person. Expect an acknowledgement within a week, and a fix released when it is written and tested rather than on a schedule I would end up missing. You will be credited unless you would rather not be.

Which versions get fixes

The newest release, and nothing else. The project is young enough that there is no sensible support window to promise.

What the threat model actually is

Read this before deciding where to run a server. These are properties of the current release, not oversights I am unaware of.

One key guards the whole server.SERVER_KEY is checked at the WebSocket upgrade, before any document data flows. That is the only authentication there is. Anyone holding it can join any workspace on that server, read everything in it, and write to it. There are no accounts, no per-member permissions, no read-only members, and no invitations. Treat the key exactly like a shared password: send it through a password manager or a private channel, never a public repository or a shared document.

Rotating the key locks everyone out. Delete the server's data/tokens.json and restart — the server mints a new key, and every member has to be given it. There is no way to revoke one member.

The server is trusted. Documents are stored and relayed in plaintext. Anyone with access to the server process or its PERSISTENCE_DIR can read every shared note. End-to-end encryption is planned, and the storage layer was designed to keep it possible, but it does not exist yet. Do not put anything on a server you do not control that you would not hand to whoever controls it.

Transport is unencrypted by default. The server speaks plain ws://, which is fine inside a home network or a VPN. Across the open internet, put a reverse proxy in front of it (Caddy, Nginx or Traefik) so members connect over wss://. Without that, the key and every keystroke cross the network in the clear.

A member is trusted once they are in. There is no server-side validation of what a client writes into the shared documents. A hostile member can tombstone everything in a workspace, or fill it with rename churn. Client-side limits contain the blast radius — every path is validated before it touches a filesystem, nothing outside the shared folder can be written, nothing is ever hard-deleted, and a bulk deletion has to be confirmed by each recipient — but the shared document itself has no guard. Only share a key with people you would give write access to a shared drive.

Data loss is bounded on purpose. This is the part the design spends most of its effort on. Removal is never destructive: a deletion is a tombstone, your copy goes to Obsidian's own trash only when the content is provably in the shared document, and otherwise into ShadowLink Recovered/. More than ten deletions in ten minutes stops and asks, defaulting to keeping your files. A file the plugin cannot prove is safe is never destroyed.

What is in scope

  • Anything that lets a client read or write a workspace without the server key.
  • Any path a remote peer can use to write outside the configured shared folder, or to make the plugin delete something without the documented confirmation.
  • Any way to make the plugin destroy content irrecoverably — that is the project's central promise, and a break in it is the most serious report you can send.
  • Anything that leaks the server key or a note's content to a third party.

What is out of scope

  • The absence of end-to-end encryption, of per-member permissions, and of server-side validation. Those are documented above and on the roadmap.
  • Denial of service against a server you were given the key to.
  • Anything requiring an attacker who already has the server key, unless it crosses a boundary the key is not supposed to open.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Overview · Phobetore/ShadowLink · GitHub
Skip to content

Security: Phobetore/ShadowLink

Security

SECURITY.md

Security policy

Reporting something

Report privately, through GitHub's private advisory form. It reaches the maintainer without the report being public first. Please do not open a normal issue for a vulnerability.

Tell me what you found, how to reproduce it, and what an attacker gets out of it. A working proof of concept is welcome but not required.

ShadowLink is maintained by one person. Expect an acknowledgement within a week, and a fix released when it is written and tested rather than on a schedule I would end up missing. You will be credited unless you would rather not be.

Which versions get fixes

The newest release, and nothing else. The project is young enough that there is no sensible support window to promise.

What the threat model actually is

Read this before deciding where to run a server. These are properties of the current release, not oversights I am unaware of.

One key guards the whole server.SERVER_KEY is checked at the WebSocket upgrade, before any document data flows. That is the only authentication there is. Anyone holding it can join any workspace on that server, read everything in it, and write to it. There are no accounts, no per-member permissions, no read-only members, and no invitations. Treat the key exactly like a shared password: send it through a password manager or a private channel, never a public repository or a shared document.

Rotating the key locks everyone out. Delete the server's data/tokens.json and restart — the server mints a new key, and every member has to be given it. There is no way to revoke one member.

The server is trusted. Documents are stored and relayed in plaintext. Anyone with access to the server process or its PERSISTENCE_DIR can read every shared note. End-to-end encryption is planned, and the storage layer was designed to keep it possible, but it does not exist yet. Do not put anything on a server you do not control that you would not hand to whoever controls it.

Transport is unencrypted by default. The server speaks plain ws://, which is fine inside a home network or a VPN. Across the open internet, put a reverse proxy in front of it (Caddy, Nginx or Traefik) so members connect over wss://. Without that, the key and every keystroke cross the network in the clear.

A member is trusted once they are in. There is no server-side validation of what a client writes into the shared documents. A hostile member can tombstone everything in a workspace, or fill it with rename churn. Client-side limits contain the blast radius — every path is validated before it touches a filesystem, nothing outside the shared folder can be written, nothing is ever hard-deleted, and a bulk deletion has to be confirmed by each recipient — but the shared document itself has no guard. Only share a key with people you would give write access to a shared drive.

Data loss is bounded on purpose. This is the part the design spends most of its effort on. Removal is never destructive: a deletion is a tombstone, your copy goes to Obsidian's own trash only when the content is provably in the shared document, and otherwise into ShadowLink Recovered/. More than ten deletions in ten minutes stops and asks, defaulting to keeping your files. A file the plugin cannot prove is safe is never destroyed.

What is in scope

  • Anything that lets a client read or write a workspace without the server key.
  • Any path a remote peer can use to write outside the configured shared folder, or to make the plugin delete something without the documented confirmation.
  • Any way to make the plugin destroy content irrecoverably — that is the project's central promise, and a break in it is the most serious report you can send.
  • Anything that leaks the server key or a note's content to a third party.

What is out of scope

  • The absence of end-to-end encryption, of per-member permissions, and of server-side validation. Those are documented above and on the roadmap.
  • Denial of service against a server you were given the key to.
  • Anything requiring an attacker who already has the server key, unless it crosses a boundary the key is not supposed to open.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Overview · Phobetore/ShadowLink · GitHub
Skip to content

Security: Phobetore/ShadowLink

Security

SECURITY.md

Security policy

Reporting something

Report privately, through GitHub's private advisory form. It reaches the maintainer without the report being public first. Please do not open a normal issue for a vulnerability.

Tell me what you found, how to reproduce it, and what an attacker gets out of it. A working proof of concept is welcome but not required.

ShadowLink is maintained by one person. Expect an acknowledgement within a week, and a fix released when it is written and tested rather than on a schedule I would end up missing. You will be credited unless you would rather not be.

Which versions get fixes

The newest release, and nothing else. The project is young enough that there is no sensible support window to promise.

What the threat model actually is

Read this before deciding where to run a server. These are properties of the current release, not oversights I am unaware of.

One key guards the whole server.SERVER_KEY is checked at the WebSocket upgrade, before any document data flows. That is the only authentication there is. Anyone holding it can join any workspace on that server, read everything in it, and write to it. There are no accounts, no per-member permissions, no read-only members, and no invitations. Treat the key exactly like a shared password: send it through a password manager or a private channel, never a public repository or a shared document.

Rotating the key locks everyone out. Delete the server's data/tokens.json and restart — the server mints a new key, and every member has to be given it. There is no way to revoke one member.

The server is trusted. Documents are stored and relayed in plaintext. Anyone with access to the server process or its PERSISTENCE_DIR can read every shared note. End-to-end encryption is planned, and the storage layer was designed to keep it possible, but it does not exist yet. Do not put anything on a server you do not control that you would not hand to whoever controls it.

Transport is unencrypted by default. The server speaks plain ws://, which is fine inside a home network or a VPN. Across the open internet, put a reverse proxy in front of it (Caddy, Nginx or Traefik) so members connect over wss://. Without that, the key and every keystroke cross the network in the clear.

A member is trusted once they are in. There is no server-side validation of what a client writes into the shared documents. A hostile member can tombstone everything in a workspace, or fill it with rename churn. Client-side limits contain the blast radius — every path is validated before it touches a filesystem, nothing outside the shared folder can be written, nothing is ever hard-deleted, and a bulk deletion has to be confirmed by each recipient — but the shared document itself has no guard. Only share a key with people you would give write access to a shared drive.

Data loss is bounded on purpose. This is the part the design spends most of its effort on. Removal is never destructive: a deletion is a tombstone, your copy goes to Obsidian's own trash only when the content is provably in the shared document, and otherwise into ShadowLink Recovered/. More than ten deletions in ten minutes stops and asks, defaulting to keeping your files. A file the plugin cannot prove is safe is never destroyed.

What is in scope

  • Anything that lets a client read or write a workspace without the server key.
  • Any path a remote peer can use to write outside the configured shared folder, or to make the plugin delete something without the documented confirmation.
  • Any way to make the plugin destroy content irrecoverably — that is the project's central promise, and a break in it is the most serious report you can send.
  • Anything that leaks the server key or a note's content to a third party.

What is out of scope

  • The absence of end-to-end encryption, of per-member permissions, and of server-side validation. Those are documented above and on the roadmap.
  • Denial of service against a server you were given the key to.
  • Anything requiring an attacker who already has the server key, unless it crosses a boundary the key is not supposed to open.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Overview · Phobetore/ShadowLink · GitHub
Skip to content

Security: Phobetore/ShadowLink

Security

SECURITY.md

Security policy

Reporting something

Report privately, through GitHub's private advisory form. It reaches the maintainer without the report being public first. Please do not open a normal issue for a vulnerability.

Tell me what you found, how to reproduce it, and what an attacker gets out of it. A working proof of concept is welcome but not required.

ShadowLink is maintained by one person. Expect an acknowledgement within a week, and a fix released when it is written and tested rather than on a schedule I would end up missing. You will be credited unless you would rather not be.

Which versions get fixes

The newest release, and nothing else. The project is young enough that there is no sensible support window to promise.

What the threat model actually is

Read this before deciding where to run a server. These are properties of the current release, not oversights I am unaware of.

One key guards the whole server.SERVER_KEY is checked at the WebSocket upgrade, before any document data flows. That is the only authentication there is. Anyone holding it can join any workspace on that server, read everything in it, and write to it. There are no accounts, no per-member permissions, no read-only members, and no invitations. Treat the key exactly like a shared password: send it through a password manager or a private channel, never a public repository or a shared document.

Rotating the key locks everyone out. Delete the server's data/tokens.json and restart — the server mints a new key, and every member has to be given it. There is no way to revoke one member.

The server is trusted. Documents are stored and relayed in plaintext. Anyone with access to the server process or its PERSISTENCE_DIR can read every shared note. End-to-end encryption is planned, and the storage layer was designed to keep it possible, but it does not exist yet. Do not put anything on a server you do not control that you would not hand to whoever controls it.

Transport is unencrypted by default. The server speaks plain ws://, which is fine inside a home network or a VPN. Across the open internet, put a reverse proxy in front of it (Caddy, Nginx or Traefik) so members connect over wss://. Without that, the key and every keystroke cross the network in the clear.

A member is trusted once they are in. There is no server-side validation of what a client writes into the shared documents. A hostile member can tombstone everything in a workspace, or fill it with rename churn. Client-side limits contain the blast radius — every path is validated before it touches a filesystem, nothing outside the shared folder can be written, nothing is ever hard-deleted, and a bulk deletion has to be confirmed by each recipient — but the shared document itself has no guard. Only share a key with people you would give write access to a shared drive.

Data loss is bounded on purpose. This is the part the design spends most of its effort on. Removal is never destructive: a deletion is a tombstone, your copy goes to Obsidian's own trash only when the content is provably in the shared document, and otherwise into ShadowLink Recovered/. More than ten deletions in ten minutes stops and asks, defaulting to keeping your files. A file the plugin cannot prove is safe is never destroyed.

What is in scope

  • Anything that lets a client read or write a workspace without the server key.
  • Any path a remote peer can use to write outside the configured shared folder, or to make the plugin delete something without the documented confirmation.
  • Any way to make the plugin destroy content irrecoverably — that is the project's central promise, and a break in it is the most serious report you can send.
  • Anything that leaks the server key or a note's content to a third party.

What is out of scope

  • The absence of end-to-end encryption, of per-member permissions, and of server-side validation. Those are documented above and on the roadmap.
  • Denial of service against a server you were given the key to.
  • Anything requiring an attacker who already has the server key, unless it crosses a boundary the key is not supposed to open.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Overview · Phobetore/ShadowLink · GitHub
Skip to content

Security: Phobetore/ShadowLink

Security

SECURITY.md

Security policy

Reporting something

Report privately, through GitHub's private advisory form. It reaches the maintainer without the report being public first. Please do not open a normal issue for a vulnerability.

Tell me what you found, how to reproduce it, and what an attacker gets out of it. A working proof of concept is welcome but not required.

ShadowLink is maintained by one person. Expect an acknowledgement within a week, and a fix released when it is written and tested rather than on a schedule I would end up missing. You will be credited unless you would rather not be.

Which versions get fixes

The newest release, and nothing else. The project is young enough that there is no sensible support window to promise.

What the threat model actually is

Read this before deciding where to run a server. These are properties of the current release, not oversights I am unaware of.

One key guards the whole server.SERVER_KEY is checked at the WebSocket upgrade, before any document data flows. That is the only authentication there is. Anyone holding it can join any workspace on that server, read everything in it, and write to it. There are no accounts, no per-member permissions, no read-only members, and no invitations. Treat the key exactly like a shared password: send it through a password manager or a private channel, never a public repository or a shared document.

Rotating the key locks everyone out. Delete the server's data/tokens.json and restart — the server mints a new key, and every member has to be given it. There is no way to revoke one member.

The server is trusted. Documents are stored and relayed in plaintext. Anyone with access to the server process or its PERSISTENCE_DIR can read every shared note. End-to-end encryption is planned, and the storage layer was designed to keep it possible, but it does not exist yet. Do not put anything on a server you do not control that you would not hand to whoever controls it.

Transport is unencrypted by default. The server speaks plain ws://, which is fine inside a home network or a VPN. Across the open internet, put a reverse proxy in front of it (Caddy, Nginx or Traefik) so members connect over wss://. Without that, the key and every keystroke cross the network in the clear.

A member is trusted once they are in. There is no server-side validation of what a client writes into the shared documents. A hostile member can tombstone everything in a workspace, or fill it with rename churn. Client-side limits contain the blast radius — every path is validated before it touches a filesystem, nothing outside the shared folder can be written, nothing is ever hard-deleted, and a bulk deletion has to be confirmed by each recipient — but the shared document itself has no guard. Only share a key with people you would give write access to a shared drive.

Data loss is bounded on purpose. This is the part the design spends most of its effort on. Removal is never destructive: a deletion is a tombstone, your copy goes to Obsidian's own trash only when the content is provably in the shared document, and otherwise into ShadowLink Recovered/. More than ten deletions in ten minutes stops and asks, defaulting to keeping your files. A file the plugin cannot prove is safe is never destroyed.

What is in scope

  • Anything that lets a client read or write a workspace without the server key.
  • Any path a remote peer can use to write outside the configured shared folder, or to make the plugin delete something without the documented confirmation.
  • Any way to make the plugin destroy content irrecoverably — that is the project's central promise, and a break in it is the most serious report you can send.
  • Anything that leaks the server key or a note's content to a third party.

What is out of scope

  • The absence of end-to-end encryption, of per-member permissions, and of server-side validation. Those are documented above and on the roadmap.
  • Denial of service against a server you were given the key to.
  • Anything requiring an attacker who already has the server key, unless it crosses a boundary the key is not supposed to open.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Overview · Phobetore/ShadowLink · GitHub
Skip to content

Security: Phobetore/ShadowLink

Security

SECURITY.md

Security policy

Reporting something

Report privately, through GitHub's private advisory form. It reaches the maintainer without the report being public first. Please do not open a normal issue for a vulnerability.

Tell me what you found, how to reproduce it, and what an attacker gets out of it. A working proof of concept is welcome but not required.

ShadowLink is maintained by one person. Expect an acknowledgement within a week, and a fix released when it is written and tested rather than on a schedule I would end up missing. You will be credited unless you would rather not be.

Which versions get fixes

The newest release, and nothing else. The project is young enough that there is no sensible support window to promise.

What the threat model actually is

Read this before deciding where to run a server. These are properties of the current release, not oversights I am unaware of.

One key guards the whole server.SERVER_KEY is checked at the WebSocket upgrade, before any document data flows. That is the only authentication there is. Anyone holding it can join any workspace on that server, read everything in it, and write to it. There are no accounts, no per-member permissions, no read-only members, and no invitations. Treat the key exactly like a shared password: send it through a password manager or a private channel, never a public repository or a shared document.

Rotating the key locks everyone out. Delete the server's data/tokens.json and restart — the server mints a new key, and every member has to be given it. There is no way to revoke one member.

The server is trusted. Documents are stored and relayed in plaintext. Anyone with access to the server process or its PERSISTENCE_DIR can read every shared note. End-to-end encryption is planned, and the storage layer was designed to keep it possible, but it does not exist yet. Do not put anything on a server you do not control that you would not hand to whoever controls it.

Transport is unencrypted by default. The server speaks plain ws://, which is fine inside a home network or a VPN. Across the open internet, put a reverse proxy in front of it (Caddy, Nginx or Traefik) so members connect over wss://. Without that, the key and every keystroke cross the network in the clear.

A member is trusted once they are in. There is no server-side validation of what a client writes into the shared documents. A hostile member can tombstone everything in a workspace, or fill it with rename churn. Client-side limits contain the blast radius — every path is validated before it touches a filesystem, nothing outside the shared folder can be written, nothing is ever hard-deleted, and a bulk deletion has to be confirmed by each recipient — but the shared document itself has no guard. Only share a key with people you would give write access to a shared drive.

Data loss is bounded on purpose. This is the part the design spends most of its effort on. Removal is never destructive: a deletion is a tombstone, your copy goes to Obsidian's own trash only when the content is provably in the shared document, and otherwise into ShadowLink Recovered/. More than ten deletions in ten minutes stops and asks, defaulting to keeping your files. A file the plugin cannot prove is safe is never destroyed.

What is in scope

  • Anything that lets a client read or write a workspace without the server key.
  • Any path a remote peer can use to write outside the configured shared folder, or to make the plugin delete something without the documented confirmation.
  • Any way to make the plugin destroy content irrecoverably — that is the project's central promise, and a break in it is the most serious report you can send.
  • Anything that leaks the server key or a note's content to a third party.

What is out of scope

  • The absence of end-to-end encryption, of per-member permissions, and of server-side validation. Those are documented above and on the roadmap.
  • Denial of service against a server you were given the key to.
  • Anything requiring an attacker who already has the server key, unless it crosses a boundary the key is not supposed to open.

There aren't any published security advisories

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Overview · Phobetore/ShadowLink · GitHub
Skip to content

Security: Phobetore/ShadowLink

Security

SECURITY.md

Security policy

Reporting something

Report privately, through GitHub's private advisory form. It reaches the maintainer without the report being public first. Please do not open a normal issue for a vulnerability.

Tell me what you found, how to reproduce it, and what an attacker gets out of it. A working proof of concept is welcome but not required.

ShadowLink is maintained by one person. Expect an acknowledgement within a week, and a fix released when it is written and tested rather than on a schedule I would end up missing. You will be credited unless you would rather not be.

Which versions get fixes

The newest release, and nothing else. The project is young enough that there is no sensible support window to promise.

What the threat model actually is

Read this before deciding where to run a server. These are properties of the current release, not oversights I am unaware of.

One key guards the whole server.SERVER_KEY is checked at the WebSocket upgrade, before any document data flows. That is the only authentication there is. Anyone holding it can join any workspace on that server, read everything in it, and write to it. There are no accounts, no per-member permissions, no read-only members, and no invitations. Treat the key exactly like a shared password: send it through a password manager or a private channel, never a public repository or a shared document.

Rotating the key locks everyone out. Delete the server's data/tokens.json and restart — the server mints a new key, and every member has to be given it. There is no way to revoke one member.

The server is trusted. Documents are stored and relayed in plaintext. Anyone with access to the server process or its PERSISTENCE_DIR can read every shared note. End-to-end encryption is planned, and the storage layer was designed to keep it possible, but it does not exist yet. Do not put anything on a server you do not control that you would not hand to whoever controls it.

Transport is unencrypted by default. The server speaks plain ws://, which is fine inside a home network or a VPN. Across the open internet, put a reverse proxy in front of it (Caddy, Nginx or Traefik) so members connect over wss://. Without that, the key and every keystroke cross the network in the clear.

A member is trusted once they are in. There is no server-side validation of what a client writes into the shared documents. A hostile member can tombstone everything in a workspace, or fill it with rename churn. Client-side limits contain the blast radius — every path is validated before it touches a filesystem, nothing outside the shared folder can be written, nothing is ever hard-deleted, and a bulk deletion has to be confirmed by each recipient — but the shared document itself has no guard. Only share a key with people you would give write access to a shared drive.

Data loss is bounded on purpose. This is the part the design spends most of its effort on. Removal is never destructive: a deletion is a tombstone, your copy goes to Obsidian's own trash only when the content is provably in the shared document, and otherwise into ShadowLink Recovered/. More than ten deletions in ten minutes stops and asks, defaulting to keeping your files. A file the plugin cannot prove is safe is never destroyed.

What is in scope

  • Anything that lets a client read or write a workspace without the server key.
  • Any path a remote peer can use to write outside the configured shared folder, or to make the plugin delete something without the documented confirmation.
  • Any way to make the plugin destroy content irrecoverably — that is the project's central promise, and a break in it is the most serious report you can send.
  • Anything that leaks the server key or a note's content to a third party.

What is out of scope

  • The absence of end-to-end encryption, of per-member permissions, and of server-side validation. Those are documented above and on the roadmap.
  • Denial of service against a server you were given the key to.
  • Anything requiring an attacker who already has the server key, unless it crosses a boundary the key is not supposed to open.

There aren't any published security advisories