Home A Weird UNC Path That can Crash Node.js
Post
Cancel

A Weird UNC Path That can Crash Node.js

Recently while fuzzing Node.js modules I found an issue which causes the process to abort when malformed Windows UNC style paths are passed into pathToFileURL(). This was initially something I looked at as a potential security issue due to the crash behavior, but after discussion with the Node.js security / WG team it was considered a bug and tracked publicly. I ended up opening a github issue here: https://github.com/nodejs/node/issues/62546

I was fuzzing path / URL related functionality inside Node.js. The input which triggered the issue was:

1
\\exa mple\share\file.txt

Passing this into:

1
require('node:url').pathToFileURL(...)

Proof of Concept

1
node -e "require('node:url').pathToFileURL('\\\\\\\\exa mple\\\\share\\\\file.txt',{windows:true})"

Result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
snoopy@snoopy-MacBookPro11-4:~/$ node -e "require('node:url').pathToFileURL('\\\\\\\\exa mple\\\\share\\\\file.txt',{windows:true})"; echo $?

  #  node[95573]: static void node::url::BindingData::PathToFileURL(const FunctionCallbackInfo<Value> &) at ../src/node_url.cc:172
  #  Assertion failed: out->set_hostname(hostname.ToStringView())

----- Native stack trace -----

 1: 0x90ace8 node::Assert(node::AssertionInfo const&) [node]
 2: 0xa2a2b7 node::url::BindingData::PathToFileURL(v8::FunctionCallbackInfo<v8::Value> const&) [node]
 3: 0x771269d8fb4d 

----- JavaScript stack trace -----

1: URL (node:internal/url:825:20)
2: pathToFileURL (node:internal/url:1659:12)
3: pathToFileURL (node:url:1022:10)
4: [eval]:1:21
5: runScriptInThisContext (node:internal/vm:219:10)
6: node:internal/process/execution:483:12
7: [eval]-wrapper:6:24
8: runScriptInContext (node:internal/process/execution:481:60)
9: evalFunction (node:internal/process/execution:315:30)
10: evalTypeScript (node:internal/process/execution:327:3)


Aborted (core dumped)
134

Instead of throwing a normal JavaScript exception, Node aborts from native code.

What pathToFileURL() Does

pathToFileURL() converts filesystem paths into file:// URLs.

Examples:

1
pathToFileURL('/tmp/test.txt')

Returns:

1
file:///tmp/test.txt

Windows examples:

1
pathToFileURL('C:\\test.txt')

Returns:

1
file:///C:/test.txt

UNC paths such as:

1
\\server\share\file.txt

Need special handling and become:

1
file://server/share/file.txt

So internally Node needs to parse:

  • hostname
  • share path
  • separators
  • validity of hostnames

That is where malformed input becomes interesting.

Root Cause

The malformed hostname section:

1
exa mple

contains a space.

Somewhere during native processing this eventually reaches:

1
out->set_hostname(hostname.ToStringView())

Which triggers an assertion rather than rejecting the input gracefully.

Expected behavior:

  • throw exception
  • reject invalid UNC host

Actual behavior:

  • process aborts

If user-controlled input reaches affected code paths, an attacker may be able to crash a Node.js process.

That could mean:

  • CLI tooling crash
  • worker restart loops
  • service instability
  • availability impact

Even when not considered a security issue upstream, process aborts are still worth fixing. After the initial crash, I checked where else pathToFileURL() was used internally.

Multiple places were interesting.

1) CLI Entry Path Handling

Node startup path handling uses:

1
pathToFileURL(mainPath)

Inside:

1
lib/internal/modules/run_main.js

2) node –check

Syntax checking flow also reaches:

1
pathToFileURL(filename)

Inside:

1
lib/internal/main/check_syntax.js

3) VM Dynamic Import Referrer

This path was interesting:

1
2
3
4
5
6
7
const vm = require('node:vm');

new vm.Script('import("node:fs")', {
  filename: process.argv[2],
  importModuleDynamically:
    vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
}).runInThisContext();

If filename is attacker controlled, it can flow into referrer normalization using pathToFileURL().

4) Public node:module APIs

Two public APIs also reached the same sink.

1
Module.findPackageJSON(..., base)

and

1
Module.findSourceMap(sourceURL)

The easiest trigger was:

1
2
const Module = require("node:module");
Module.findSourceMap(process.argv[2]);

Run:

1
2
3
node run.js "\\ex ample\\share\\file.txt"

Crash.

I also reproduced this on Windows where the process aborted with the same assertion stack trace. You can see it below

I initially tried to report it through HackerOne under the Node.js program. However, low signal restrictions in hackerone prevented submission. After that I contacted the Node.js security / WG folks through their Slack channels and shared details. Their view was that this should be treated as a bug rather than a security issue.

That is fair from a triage perspective:

  • no RCE
  • no memory corruption
  • no privilege escalation
  • controlled malformed input causes abort

Still, if externally reachable, crashes can matter operationally. This was a fun find because it started as random fuzzing and turned into:

  • root cause review
  • call graph tracing
  • multiple reachable sinks
  • upstream bug confirmation

Sometimes the best bugs are not flashy memory corruption bugs.Sometimes it is just one malformed path and an assertion waiting behind it. Learning security while segfaulting through life.

This post is licensed under CC BY 4.0 by the author.