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:
- Resolves both to absolute paths
- Computes the normalized relative path between them
- 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:
assertSafeExtensionId— syntactic validationresolvePathWithinRoot— 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:
assertSafeExtensionIdenforces whitelist-based ID sanitization that rejects path separators and traversal tokens - Path confinement layer:
resolvePathWithinRootguarantees physical containment within the extensions root via normalized relative path computation - Defense in depth:
resolveExtensionPathWithinRootcomposes 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.tsprevents 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.
Can symbolic links escape the extensions root directory?
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →