A new net permission model: allow scoping in --allow-net
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Idoneità per principianti
- 18/100
- Tipo di issue
- Funzionalità
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Stack tecnologico
- javascript
Direzione di ricerca
Start with the rollout plan and the per-transport tests named as test/parallel/test-permission-net-*; the issue describes changes across native permission checks, TCP, UDP, pipes, DNS, QUIC, DTLS, config, and JavaScript APIs. First read the existing permission checks and one transport test, then compare the open questions and staged PR order. Done means the agreed scope is implemented across the required transports and surfaces, with the listed bypass, config, audit, and permission consistency cases covered.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Summary
Today --allow-net is all-or-nothing. This proposal lets it take a list: --allow-net=api.example.com:443,10.0.0.0/8,*.example.com. The list accepts host names, IPv4, bracketed IPv6, CIDR and *. wildcards, each with an optional port.
Bare --allow-net keeps its current meaning. Checks run in the native layer (TCP, UDP, pipes, DNS, QUIC, DTLS), and a lookup token ties an allowed name to the IPs it resolved to. process.permission.has('net', 'host:port') exposes the same check to JS.
The first iteration has no --deny-net and no proxy awareness.
Motivation and prior art
- Today
netis all-or-nothing:--allow-netgrants every host, every port and every socket. - Users want least privilege, for example allowing only
api.example.com:443and their database. - Prior art: Deno's
--allow-net=host:portand--deny-net. - nodejs/node#66117 closed the
Socket(fd)bypass; only stdio and the IPC channel are exempt.
Goals
- Limit network access to specific hosts, IPs, CIDR ranges and ports.
- Bare
--allow-net, and no--allow-netat all, behave exactly as today. - Enforce in the native layer for every transport (TCP, UDP, pipes, DNS, QUIC, DTLS), so it can't be bypassed from JS through
_handle. - No cost when the permission model is off; bare
--allow-netstays a single boolean check. process.permission.has('net', 'host:port')gives the same answer as the native check.- The config file accepts the list (
"allow-net": ["api.example.com:443"]);truekeeps working. - Audit mode reports the denied
host:port.
Non-goals (first iteration)
- No
--deny-net. - No proxy awareness: with
HTTP_PROXYset, the allowed host is the proxy. - No change to bare
--allow-net. Sockets created from an fd (nodejs/node#66117) are allowed only with bare--allow-net, since there is no host to match.
Compatibility and release
- semver-minor: an existing flag gains a value form. Bare
--allow-net, no--allow-net, and"allow-net": trueall behave as today. - Stability 1.1 would allow semantic changes in a minor, but none are needed.
- Lands on
main(v27). v24 and older don't have--allow-net(added in v25.0.0). - Backport to v26: needs input from the Release WG, since v26 is expected to stay stable.
Resource model
*.example.commatches names at any depth (RFC 4592), but notexample.comitself.- IPv4-mapped IPv6 addresses are normalized:
::ffff:192.0.2.1is192.0.2.1. - A trailing dot is ignored:
example.com.isexample.com. - Names are case-insensitive (RFC 1035, RFC 4343).
Surfaces
- CLI:
--allow-net=<rule>[,<rule>...]. A rule ishost[:port],ip[:port],[ipv6][:port],cidr,*.domain[:port]orunix:<path>(see Q4). - Config file:
"permission": { "allow-net": ["api.example.com:443"] }, ortrue. process.permission.has('net', 'host:port'): same answer as the native check.permission.drop('net', rule): only drops a rule exactly as it was granted.drop('net')drops everything.- Audit: the published message names the
host:port.
Enforcement
UDP:
- Unconnected
sendchecks the destination on every packet, since each packet can go to a different host. - A connected socket checks once at
connect; itssendcalls are not checked again.
DNS: dns.resolve* checks the queried name but creates no token. A connect to an IP is a connect to that IP, not to a host, so it needs an IP entry. Code that also wants a name check can call process.permission.has('net', 'api.example.com') itself.
Custom lookup: with --allow-net=api.example.com, fetch('https://api.example.com') works, because Node's own lookup resolves the name. If the code sets its own lookup function, for example new Agent({ connect: { lookup } }) from undici, that function returns the IP instead of Node. Node can't tell whether that IP really belongs to api.example.com, so it checks the IP: the request is denied unless the IP is allowed too.
Security
- Threat model unchanged: a seat belt for trusted code, not a sandbox.
- What lists add: trusted code can't accidentally reach a host outside the list.
- Known limits:
- An allowed name is trusted wherever its DNS answer points, including internal IPs (DNS rebinding).
- An allowed name's IP can serve other sites through TLS SNI or the Host header (domain fronting).
- Proxies and fd sockets are covered under Non-goals.
- Hardening: checks are native, so
_handlecan't bypass them, and the token can't be forged.
Performance
Principle: permission mode accepts the cost of checking, and every check is made as cheap as possible. The cost is only paid in list mode; bare --allow-net stays a boolean.
Matchers (first iteration):
- Exact IPs: a hash set.
- CIDR: a prefix table.
*.names: a suffix tree.
These can change later for performance.
Generation counter: each rule add or drop increments a counter per Environment. A token stores the counter value and the rule its host matched. At connect, if the values are equal, only ip ∈ token and the port are checked; otherwise a full check runs. The counter is compared before the cached rule is used, and it isn't exposed to JS.
Rollout and testing
PR order (every release stays safe):
- tcp, udp, pipe and cares checks pass host, port and direction, one PR per transport. The only visible change is more detail in audit messages.
- Lookup token, and
connect(..., token). - Matchers and the generation counter.
- Switch: parser, config file,
has(),drop, audit, docs. In list mode, a check with no host denies. - QUIC and DTLS, as separate PRs. Until they land, both deny in list mode.
Tests: extend the per-transport files (test/parallel/test-permission-net-*). Add bypass tests (_handle, custom lookup, ::ffff:, trailing dot, case), has() vs native check, a drop invalidating tokens, Happy Eyeballs, undici IP + servername, config file, audit.
Benchmark: benchmark/net with permission off, bare, and a list, plus UDP send.
Notes
Workers: each worker has its own permission state, built from its own flags. A drop only affects the thread that calls it: running workers keep their rules, and new workers start again from the CLI list.
Child processes: spawning needs --allow-child-process. A Node.js child gets the parent's permission flags through NODE_OPTIONS, so the same --allow-net=<list> applies, and runtime drops don't carry over. Non-Node children (for example curl) aren't restricted at all. The list has to survive NODE_OPTIONS quoting, e.g. unix: paths that contain spaces.
Open questions
Q1: Name vs IP
Importance: Critical
Question: Hostnames can only be enforced at DNS lookup time (cares_wrap); a direct connect('192.0.2.1') needs an IP entry. Deno checks names at lookup and links the resolved IPs to connect with a per-socket token; resolution is also used for deny checks. Is that acceptable for Node, or should connect re-resolve and compare?
Problem: connect only sees the IP, so a name rule cannot match it. And connect can be reached without a lookup.
Recommendation: a native token {host, ips}, created by the lookup and checked by connect.
example.com ▶ cares_wrap ▶ tcp_wrap ──▶ ip in token? ── yes ─▶ check host
check host (ip+token) └─ no ──▶ check ip
make token
192.0.2.1 ───────────────▶ tcp_wrap ──▶ no token ───────────▶ check ip
(IP literal, custom lookup, _handle)
- Binding change:
tcp_wrap.connect(req, ip, port)→connect(req, ip, port[, token]), for bothconnectandconnect6. The token is optional, so existing calls don't break, but both TCP paths innet.jsmust pass it:internalConnectand Happy Eyeballs (internalConnectMultiple). Otherwise name rules never match. - Port: unlike Deno, not in the token. The lookup never sees a port; connect always does. So the lookup matches the host, and connect checks
host:port.
Alternatives considered:
| Option | Result | Why |
|---|---|---|
| Check the name only at lookup | ✗ | IP literal, custom lookup and _handle skip it |
| Re-resolve at connect | ✗ | second answer can differ; extra latency |
| IP set per Environment | ✗ | an allowed name opens its IPs to every socket |
| JS-only check | ✗ | bypassed through _handle |
| Native lookup+connect | ✗ | breaks lookup and Happy Eyeballs |
Native token {host, ips} |
✓ | per socket, cannot be forged, only in list mode |
Q2: Connect vs listen
Importance: Critical
Question: should a host list also control listen()?
Problem: a listen resource is a local address, not a remote host.
Options:
| Option | Example | Pros | Cons |
|---|---|---|---|
| A. One list | --allow-net=api.example.com,0.0.0.0:3000 |
one grammar, no new flag, listen can be restricted | no connect-only rule; servers must list their bind address |
| B. Outbound only | --allow-net=api.example.com |
servers keep working, no listen syntax | listen cannot be restricted |
| C. Separate listen list | --allow-net=api.example.com --allow-net-listen=:3000 |
explicit direction, each side restricted on its own | new flag; two lists to keep in sync |
Prior art: Deno uses A.
A and C both need Bind to pass addr:port; today it passes "".
Q3: Port ranges
Importance: Critical
Question: should example.com:8000-8100 be supported? Leaning yes.
Q4: Unix sockets and named pipes
Importance: Critical
Question: which permission controls a unix socket or named pipe: net, fs, or both?
Problem: a unix socket is both a file and a network connection. Today only kNet is checked, and bare --allow-net opens every socket path.
Options:
| Option | Example | Pros | Cons |
|---|---|---|---|
| A. net only | --allow-net=unix:/run/app.sock |
one flag | ignores fs rules |
| B. fs only | --allow-fs-read/write=/run/app.sock |
no new syntax | network uncontrolled |
| C1. both, path in each | fs + --allow-net=unix:/run/app.sock |
broad fs rules don't open other sockets | path written twice |
| C2. both, path only in fs | fs + --allow-net=unix |
shorter | broad fs opens every socket |
Recommendation: C1.
Abstract sockets have no file, so only the net rule applies.
Q5: IPv6 zone
Importance: High
Question: does [fe80::1] match fe80::1%eth0?
Problem: a link-local address is only unique per interface: fe80::1%eth0 and fe80::1%wlan0 can be two different machines. A rule without a zone either matches both (too broad) or neither (useless).
Options:
- A. A rule without a zone matches every interface.
- B. The rule must name the zone:
[fe80::1%eth0].
Q6: CIDR with a port
Importance: Medium
Question: should 10.0.0.0/8:443 be allowed?
Problem: "any host in my network, only on port 443" is a real need, but CIDR notation (RFC 4632) has no port form, so this syntax would be our own.
Options:
- A. Allow
cidr:port, for example10.0.0.0/8:443and[2001:db8::/32]:443. - B. A CIDR rule always means all ports.
Q7: Windows pipe prefix
Importance: Medium
Question: which prefix names a Windows named pipe in a net rule?
Problem: a Windows pipe (\\.\pipe\app) isn't a unix socket, but Node handles both through the same PipeWrap (libuv uv_pipe_t).
Options:
- A.
unix:\\.\pipe\app, as in Deno: one prefix, but a misleading name. - B.
pipe:\\.\pipe\app: a clear name, but two prefixes.
Recommendation: A.
Q8: IDN
Importance: Low
Question: should bücher.example work as a rule?
Problem: DNS carries only ASCII, so the name travels as xn--bcher-kva.example. If the rule and the lookup are spelled differently, the rule never matches. Converting the name also lets look-alike names (homographs) through.
Options:
- A. Convert:
bücher.examplebecomesxn--bcher-kva.examplebefore matching. Friendly, but look-alike names get through. - B. Reject: a non-ASCII rule is an error, and users write the
xn--form. Simple and safe.
Q9: Counter width
Importance: Low
Question: should the generation counter be uint32 or uint64?
Problem: if the counter wraps, old tokens become valid again. At a million drops per second, uint32 wraps in 71.6 minutes; uint64 takes 585,000 years.
Future work
--deny-net, with deny checks after resolution (catches a name that resolves to a denied IP).- Proxy awareness: check the target host behind
HTTP_PROXY, not just the proxy. - Faster matchers, if benchmarks show a need.
- Lingua principale
- JavaScript
- Stelle
- 123k
- Fork
- 39.3k
- Merge medio
- 4g 16h
- PR unite (30g)
- 231
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di nodejs/node
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
I maintainer di solito rispondono entro 1 giorno
-
util.parseEnv and --env-file keep a leading UTF-8 BOM in the first keyForse già presa @helmipradita l’ha presa 2 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
node:internal/inspector/network_http: `TypeError: Missing dataLength` in event when response uses `setEncoding()`Forse già presa @lazerg l’ha presa 26 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
nodejs/node#66551 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
[Docs] `process.loadEnvFile()` does not document behaviour when variables already existForse già presa @Sepandard l’ha presa 36 giorni fa. Apertadoc
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 90/100
I maintainer di solito rispondono entro 1 giorno
-
Stream.prototype.forEach will block in first promise in queue before read more chunkForse già presa @mmustafasenoglu l’ha presa 36 giorni fa. Apertadoc
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
I maintainer di solito rispondono entro 1 giorno
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
I maintainer di solito rispondono entro 1 giorno
-
【错误报告】界限突破·于吉「蛊惑」结算崩溃:reguhuo_final 中 chooseToDiscard().forResult() 可能返回 undefined(result.bool 未判空)Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 Meno di un'ora Idoneità per principianti 88/100
I maintainer di solito rispondono entro 1 giorno
-
ai-driven-fuzzing bug claude Minor
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
linagora/twake-drive#4494 ·
I maintainer di solito rispondono entro 1 giorno
-
[quality] /resources/metrics draws the single-point member-companies chart on the left axisForse già presa @hivecommons-hive l’ha presa oggi. Apertaagent/quality stage/040-covered-by-pr testing
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
I maintainer di solito rispondono entro 1 giorno