Skip to content

Commit b15f809

Browse files
authored
Add a configurable node_modules allow-list (#47)
Adds an allow-list for node_modules that directly load user code and so are suitable scripts to use with --inspect. Additional modules can be included by adding them to the WRAPPER_ALLOWED environment variable, separated by spaces.
1 parent 7d47421 commit b15f809

2 files changed

Lines changed: 78 additions & 13 deletions

File tree

nodejs/wrapper.go

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ limitations under the License.
2222
// script, this wrapper strips out and propagates `--inspect`-like arguments
2323
// via `NODE_DEBUG`. When executing an app script, this wrapper then inlines
2424
// the `NODE_DEBUG` when found.
25+
//
26+
// A certain set of node_modules scripts are treated as if they are application scripts.
27+
// The WRAPPER_ALLOWED environment variable allows identifying node_modules scripts
28+
// that should be treated as application scripts, meaning that they load and execute
29+
// the user's scripts directly.
2530
package main
2631

2732
import (
@@ -37,6 +42,9 @@ import (
3742
"github.com/sirupsen/logrus"
3843
)
3944

45+
// the next.js launcher loads user scripts directly
46+
var allowedNodeModules = []string{"node_modules/.bin/next"}
47+
4048
// nodeContext allows manipulating the launch context for node.
4149
type nodeContext struct {
4250
program string
@@ -47,7 +55,7 @@ type nodeContext struct {
4755
func main() {
4856
env := envToMap(os.Environ())
4957
logrus.SetLevel(logrusLevel(env))
50-
58+
5159
logrus.Debugln("Launched: ", os.Args)
5260

5361
// suppress npm warnings when node on PATH isn't the node used for npm
@@ -79,31 +87,36 @@ func run(nc *nodeContext, stdin io.Reader, stdout, stderr io.Writer) error {
7987
return fmt.Errorf("could not unwrap: %w", err)
8088
}
8189
logrus.Debugln("unwrapped: ", nc.program)
82-
90+
8391
if !isEnabled(nc.env) {
8492
logrus.Info("wrapper disabled")
8593
return nc.exec(stdin, stdout, stderr)
8694
}
8795

88-
// Use an absolute path in case we're being run within a node_modules directory
89-
// If there's an error, then hand off immediately to the real node.
96+
// script may be "" such as when the script is piped in through stdin
9097
script := findScript(nc.args)
91-
if abs, err := filepath.Abs(script); err == nil {
92-
script = abs
93-
} else {
94-
logrus.Warn("could not access script: ", err)
95-
return nc.exec(stdin, stdout, stderr)
96-
}
98+
if script != "" {
99+
// Use an absolute path in case we're being run within a node_modules directory
100+
// If there's an error, then hand off immediately to the real node.
101+
if abs, err := filepath.Abs(script); err == nil {
102+
script = abs
103+
} else {
104+
logrus.Warn("could not access script: ", err)
105+
return nc.exec(stdin, stdout, stderr)
106+
}
107+
}
97108
logrus.Debugln("script: ", script)
98109

110+
// If NODE_DEBUG is set then our parent process was this wrapper, and
111+
// NODE_DEBUG contains the --inspect* argument provided back then.
99112
nodeDebugOption, hasNodeDebug := nc.env["NODE_DEBUG"]
100113
if hasNodeDebug {
101114
logrus.Debugln("found NODE_DEBUG=", nodeDebugOption)
102115
}
103116

104-
// if we're about to execute the application script, install the NODE_DEBUG
117+
// If we're about to execute the application script, install the NODE_DEBUG
105118
// arguments if found and go
106-
if isApplicationScript(script) || script == "" {
119+
if script == "" || isApplicationScript(script) || isAllowedNodeModule(script, nc.env) {
107120
if hasNodeDebug {
108121
nc.stripInspectArgs() // top-level debug options win
109122
nc.addNodeArg(nodeDebugOption)
@@ -255,6 +268,23 @@ func isApplicationScript(path string) bool {
255268
!strings.HasSuffix(path, "/bin/npm")
256269
}
257270

271+
// isAllowedNodeModule returns true if the script is an allowed node_module, meaning
272+
// one that is or directly launches the user's code.
273+
func isAllowedNodeModule(path string, env map[string]string) bool {
274+
allowedList := allowedNodeModules
275+
if v, found := env["WRAPPER_ALLOWED"]; found {
276+
split := strings.Split(v, " ")
277+
allowedList = append(allowedList, split...)
278+
}
279+
for _, allowed := range allowedList {
280+
if strings.HasSuffix(path, allowed) {
281+
logrus.Infof("script %q matches %q from allowed node_modules", path, allowed)
282+
return true
283+
}
284+
}
285+
return false
286+
}
287+
258288
// envToMap turns a set of VAR=VALUE strings to a map.
259289
func envToMap(entries []string) map[string]string {
260290
m := make(map[string]string)

nodejs/wrapper_test.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,30 @@ func TestIsApplicationScript(t *testing.T) {
146146
}
147147
}
148148

149+
func TestIsAllowedNodeModule(t *testing.T) {
150+
tests := []struct {
151+
script string
152+
env map[string]string
153+
expected bool
154+
}{
155+
{"./node_modules/lib/script.js", nil, false},
156+
{"./node_modules/.bin/next", nil, true},
157+
{"node_modules/nodemon/nodemon.js", nil, false},
158+
{"node_modules/nodemon/nodemon.js", map[string]string{"WRAPPER_ALLOWED": "foo bar"}, false},
159+
{"node_modules/nodemon/nodemon.js", map[string]string{"WRAPPER_ALLOWED": "nodemon/nodemon.js"}, true},
160+
{"node_modules/nodemon/nodemon.js", map[string]string{"WRAPPER_ALLOWED": "foo nodemon/nodemon.js bar"}, true},
161+
}
162+
163+
for _, test := range tests {
164+
t.Run(test.script, func(t *testing.T) {
165+
result := isAllowedNodeModule(test.script, test.env)
166+
if result != test.expected {
167+
t.Errorf("expected %v but got %v", test.expected, result)
168+
}
169+
})
170+
}
171+
}
172+
149173
func TestEnvFromMap(t *testing.T) {
150174
tests := []struct {
151175
description string
@@ -535,7 +559,7 @@ done
535559
},
536560

537561
// node_module scripts should have --inspect stripped and propagated,
538-
// and NODE_DEBUG should never be overwritten
562+
// and NODE_DEBUG should never be overwritten, EXCEPT for allowed modules
539563
{
540564
description: "node_modules script: passed through",
541565
args: []string{"node_modules/script.js"},
@@ -552,6 +576,17 @@ done
552576
env: map[string]string{"WRAPPER_ENABLED": "0"},
553577
expected: "--inspect=9229\n./node_nodules/script.js\n",
554578
},
579+
{
580+
description: "node_modules script: inspect left alone for next.js launcher",
581+
args: []string{"--inspect=9229", "./node_nodules/.bin/next"},
582+
expected: "--inspect=9229\n./node_nodules/.bin/next\n",
583+
},
584+
{
585+
description: "node_modules script: inspect left alone with WRAPPER_ALLOWED=script.js",
586+
args: []string{"--inspect=9229", "./node_nodules/script.js"},
587+
env: map[string]string{"WRAPPER_ALLOWED": "script.js"},
588+
expected: "--inspect=9229\n./node_nodules/script.js\n",
589+
},
555590
{
556591
description: "node_modules script with inspect: seeds NODE_DEBUG",
557592
args: []string{"--inspect", "node_modules/script.js"},

0 commit comments

Comments
 (0)