Plexcord LogoPlexcord
Best Practices

Security

Write safe Plexcord plugins. Prevent XSS, validate user input, protect tokens, handle API errors, and avoid common vulnerabilities.

Security

Plugin security matters. A vulnerable plugin runs in Discord's renderer process with access to the user's account, messages, and personal data. These guidelines prevent common security pitfalls.


Never Use innerHTML with User Data

This is the most common vulnerability. Inserting user-controlled strings as HTML creates XSS opportunities:

// ❌ DANGEROUS: message.content could contain <script>alert(1)</script>
element.innerHTML = `<div>${message.content}</div>`;

// ✅ Safe: text only, no HTML interpretation
element.textContent = message.content;

// ✅ Also safe via React
<div>{message.content}</div>          // React auto-escapes
<div dangerouslySetInnerHTML=... />   // ❌ Never use this with user data

Safe alternatives

// Create elements programmatically
const div = document.createElement("div");
div.textContent = userInput;  // Safe: treated as text, not HTML
parent.appendChild(div);

// Or build React elements
const content = React.createElement("span", null, userInput);

Validate All User Inputs

Settings values come from the user directly. Never trust them without validation:

// ❌ Unsafe: trusts the setting blindly
const limit = settings.store.maxItems;
const items = array.slice(0, limit);  // Could be NaN, negative, or Infinity

// ✅ Safe: validates and clamps
const rawLimit = settings.store.maxItems;
const limit = Number.isInteger(rawLimit) && rawLimit > 0
    ? Math.min(rawLimit, 1000)   // Cap at reasonable max
    : 10;                         // Fallback default
const items = array.slice(0, limit);
// ❌ Unsafe: URL setting passed directly to fetch
await fetch(settings.store.webhookUrl);

// ✅ Validate URL format and scheme first
function isValidHttpsUrl(url: string): boolean {
    try {
        const u = new URL(url);
        return u.protocol === "https:";
    } catch {
        return false;
    }
}

if (isValidHttpsUrl(settings.store.webhookUrl)) {
    await fetch(settings.store.webhookUrl);
} else {
    logger.warn("Invalid webhook URL:", settings.store.webhookUrl);
}

Never Log or Store Tokens

The user's Discord token grants full account access. Never touch it:

// ❌ Extremely dangerous
const token = TokenModule.getToken();
await DataStore.set("token", token);
logger.info("Token:", token);

// ✅ Never interact with auth tokens
// If you need to make API calls, use the built-in RestAPI
import { RestAPI } from "@webpack/common";
await RestAPI.get({ url: "/users/@me" });  // Token is handled internally

Storing, logging, or transmitting the user's token is a bannable offense and a serious security vulnerability. Plexcord's review process will reject any plugin that touches tokens.


Handle API Errors Gracefully

Network calls fail. Always handle RestAPI rejections:

// ❌ Unhandled: a 429 rate limit or 403 will crash the plugin
const response = await RestAPI.get({ url: `/channels/${channelId}` });
processData(response.body);

// ✅ Handle errors explicitly
try {
    const response = await RestAPI.get({ url: `/channels/${channelId}` });
    if (response.ok) {
        processData(response.body);
    } else {
        logger.warn(`API returned ${response.status} for channel ${channelId}`);
    }
} catch (e) {
    logger.error("API request failed:", e);
}

Don't Expose Private User Data

Be careful what your plugin logs or sends elsewhere:

// ❌ Leaks private message content to the console
logger.info("Processing message:", message);  // Contains content, author, etc.

// ✅ Log only what's needed for debugging
logger.debug("Processing message ID:", message.id, "in channel:", message.channel_id);

If your plugin sends data to an external service (webhook, analytics, etc.), be explicit in the plugin description and make it opt-in.


Validate DataStore Data on Read

Data in DataStore could be stale or malformed from an older plugin version:

interface SavedEntry {
    id: string;
    value: number;
    createdAt: number;
}

async function loadEntries(): Promise<SavedEntry[]> {
    const raw = await DataStore.get("MyPlugin_entries");

    // ❌ No validation: crashes if data is wrong type
    return raw;

    // ✅ Validate structure before use
    if (!Array.isArray(raw)) return [];

    return raw.filter((item): item is SavedEntry =>
        typeof item?.id === "string" &&
        typeof item?.value === "number" &&
        typeof item?.createdAt === "number"
    );
}

Patch Safety

The patch system modifies Discord's source code directly. Follow these rules:

RuleWhy
Use very specific find stringsAvoid patching the wrong module
Test in DevTools before shippingConfirm match is unique
Use predicate to limit scopeOnly apply when needed
Keep replacements minimalSmaller changes break less
Handle patched code throwingWrap patched calls in try/catch
patches: [{
    find: "very.specific.unique.string.in.module",
    replacement: {
        match: /exactPattern\((\i)\)/,
        replace: "($self.safeWrapper($1) ?? exactPattern($1))"
    }
}],

safeWrapper(arg: any) {
    try {
        return myTransform(arg);
    } catch (e) {
        logger.error("Transform failed, falling back:", e);
        return null; // Fallback to original behavior
    }
}

Security Checklist

Before publishing your plugin, verify:

  • No innerHTML with user-controlled strings
  • All settings values validated before use
  • No token access (TokenModule.getToken() never called)
  • All RestAPI calls wrapped in try/catch
  • No sensitive data logged (message content, user IDs when not needed, etc.)
  • DataStore reads validated before use
  • External requests are optional and documented
  • Patches use specific find strings

On this page