Extension Installation Path Validation Security: How Modly Prevents Directory Traversal Attacks

Modly uses a layered defense-in-depth strategy combining strict extension ID sanitization, root-confined path resolution, and reserved internal directory naming to block path-traversal and filesystem escape attacks during extension installation.

The lightningpixel/modly Electron application handles third-party extensions that execute with system-level privileges. This makes extension installation path validation security critical—any vulnerability could allow malicious extensions to write files outside the designated extensions directory, overwrite system files, or extract sensitive data. The codebase addresses this through a dedicated guard module that enforces multiple independent validation layers.

Strict Extension ID Sanitization

The first line of defense lives in electron/main/extension-path-guard.ts. The assertSafeExtensionId function validates that every extension identifier meets rigid syntactic constraints before it ever touches the filesystem:

  • Must be non-empty and lowercase
  • Permitted characters only: [a-z0-9._-]
  • Explicitly forbidden: path separators (/, \), absolute-path syntax (leading / or drive letters), and the directory traversal tokens . or ..

Any violation throws immediately, halting the installation process. This whitelist approach eliminates entire categories of injection attacks at the input boundary.

// Valid: resolves successfully
assertSafeExtensionId('mesh-processor');

// Throws: "Extension id \"../escape\" must not contain path separators"
assertSafeExtensionId('../escape');

// Throws: "Extension id \"UPPERCASE\" must be lowercase"
assertSafeExtensionId('UPPERCASE');

Root-Confined Path Resolution

The resolvePathWithinRoot function implements the physical confinement guarantee. It accepts a root directory (the extensions folder) and a leaf path, then:

  1. Resolves both to absolute paths
  2. Computes the normalized relative path between them
  3. Rejects the operation if the relative result is empty, climbs upward via .., or resolves as absolute

This ensures the final path strictly resides inside the designated root, regardless of symlink tricks, case-sensitivity issues, or platform-specific path behaviors.

const extensionsRoot = '/usr/local/modly/extensions';

// Valid: returns '/usr/local/modly/extensions/mesh-processor'
resolvePathWithinRoot(extensionsRoot, 'mesh-processor');

// Throws: escapes the root directory
resolvePathWithinRoot(extensionsRoot, '../escape');
resolvePathWithinRoot(extensionsRoot, 'subdir/../../escape');

Combined Safe Resolution Pipeline

The resolveExtensionPathWithinRoot function composes both checks into a single defensive pipeline. Every extension directory path flows through:

  1. assertSafeExtensionId — syntactic validation
  2. resolvePathWithinRoot — physical confinement

This sequential enforcement means an attacker must defeat both layers simultaneously to achieve path traversal—a practical impossibility given their orthogonal designs.

import {
  resolveExtensionPathWithinRoot,
  buildExtensionBackupPath,
  buildExtensionStagingPath,
} from './extension-path-guard'

const extensionsRoot = '/usr/local/modly/extensions';

// Final resolved path for extension storage
const safePath = resolveExtensionPathWithinRoot(extensionsRoot, 'mesh-process');
// → '/usr/local/modly/extensions/mesh-process'

// Backup directory for atomic rollback capability
const backupPath = buildExtensionBackupPath(extensionsRoot, 'mesh-process', '1654321000');
// → '/usr/local/modly/extensions/.modly-backup-mesh-process-1654321000'

// Staging directory for atomic installation
const stagingPath = buildExtensionStagingPath(extensionsRoot, 'mesh-process', 'tmp42');
// → '/usr/local/modly/extensions/.modly-staging-mesh-process-tmp42'

Reserved Internal Directory Naming

The guard module defines three reserved prefixes for installer-internal use:

  • .modly-backup-{id}-{timestamp}
  • .modly-staging-{id}-{token}
  • .modly-incomplete-{id}

Because valid extension IDs cannot start with a dot, these directories are automatically excluded from extension discovery mechanisms. This separation prevents:

  • Accidental loading of partially-installed extensions
  • Exposure of backup data containing old versions
  • Race condition exploits between staging and activation phases

The helper functions buildExtensionBackupPath and buildExtensionStagingPath reuse the safe resolution logic, ensuring auxiliary paths maintain the same security guarantees.

Comprehensive Test Coverage

The companion suite in electron/main/extension-path-guard.test.ts validates both positive and negative cases:

  • Valid inputs: lowercase IDs with dots, underscores, hyphens
  • Rejection categories: empty strings, path separators, traversal sequences (..), uppercase letters, illegal characters (!@#%), absolute paths

This test-driven approach catches regressions and documents the exact boundaries of acceptable input.

Integration Across the Installation Pipeline

The guard utilities propagate through multiple installation modules:

File Security Function
electron/main/extension-path-guard.ts Core validation and resolution primitives
electron/main/extension-path-guard.test.ts Automated behavioral verification
electron/main/extension-install-utils.ts Manifest validation and path builder consumption
electron/main/builtin-sync.ts Atomic installation coordination using staging/backup paths

Summary

  • Input validation layer: assertSafeExtensionId enforces whitelist-based ID sanitization that rejects path separators and traversal tokens
  • Path confinement layer: resolvePathWithinRoot guarantees physical containment within the extensions root via normalized relative path computation
  • Defense in depth: resolveExtensionPathWithinRoot composes both layers sequentially
  • Operational security: Reserved dot-prefix naming isolates installer-internal directories from discovery
  • Verified guarantees: Comprehensive test coverage in extension-path-guard.test.ts prevents regression

Frequently Asked Questions

What happens if an extension ID contains uppercase letters?

The assertSafeExtensionId function throws an error with message "Extension id must be lowercase". This strict requirement prevents case-sensitivity confusion attacks where "Extension" and "extension" might resolve to different paths on case-insensitive filesystems.

No. The resolvePathWithinRoot function computes paths using normalized relative resolution after converting both root and leaf to absolute paths. Symlink resolution occurs during the absolute path conversion, and the subsequent relative path check detects any escape attempts regardless of how they were constructed.

How does Modly prevent discovery of partially-installed extensions?

Reserved prefixes starting with dots (.modly-staging-, .modly-incomplete-) are used for installation temporaries. Since valid extension IDs cannot begin with a dot, the discovery mechanism naturally excludes these directories without additional filtering logic.

Is the backup path generation equally protected as regular extension paths?

Yes. The buildExtensionBackupPath and buildExtensionStagingPath functions internally call resolveExtensionPathWithinRoot, ensuring the extension ID undergoes identical sanitization and confinement checks before the backup or staging suffix is appended.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →