You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Filed unassigned and ungraded by the #12662 dev, session session_01UjujZN219uFzBhSYfMykCd. ⛔ Not graded, not routed. Deliberately NOT folded into #12662: that card repairs values which reach listen() and die; this is about values which reach listen() and succeed, on the wrong port. Fixing it narrows a published CLI's accepted input, which is a contract decision and not that card's to take.
Measured
os serve reads its port with parseInt(flags.port) — before #12662 and after it, deliberately. parseInt stops at the first character it cannot use and returns what it has:
input
parseInt
binds today
3e3
3
port 3
1e10
1
port 1
3000abc
3000
port 3000
0x0BB8
3000
port 3000 (hex, read as 3000)
3000.0
3000
port 3000
+3000
3000
port 3000
" 3000"
3000
port 3000 (leading whitespace)
Measured with node -e on this checkout, Node v22.22.2, against both parseInt and a real net.createServer().listen().
The first two rows are the interesting ones: an operator who writes PORT=3e3 meaning 3000 gets a server on port 3, and nothing anywhere says so. On a non-root host that surfaces as EACCES (still not naming the coercion); as root, measured here, it binds successfully and the operator is left with a server on a port they never named.
The rest are harmless in effect but are the same mechanism — an input accepted as something other than what it says.
#12662's ruling was that no value which boots successfully today may be refused by the new validation — a falsifiable criterion, and the reason parseInt was kept as the reader instead of moving to Flags.integer (whose /^-?\d+$/ refuses every row above). serve-port-validation.test.ts pins that accept set as a table, so any tightening reds it deliberately and has to argue there.
Not established here
Severity not judged, and no claim about how often anyone writes an exponent or a hex literal into PORT.
⚠️The question is a contract one, and it is genuinely open: should the port input be strict (a decimal integer, whitespace trimmed), refusing everything in the table above? That narrows what boots, so it needs the accept-set change reviewed, a changeset that states the FROM/TO, and a decision on whether whitespace is trimmed rather than refused (production PORT carries it). ⛔ Not decided here.
Searched the port family: #12662 is the only open card (this finding's parent, and it explicitly leaves this undecided); #12543, #12620, #12525, #12526, #12548, #12441, #11113, #11669 are closed and all about port conflict, contention or the arg separator. Nothing covers parseInt coercion of a valid-looking port.
Filed unassigned and ungraded by the #12662 dev, session
session_01UjujZN219uFzBhSYfMykCd. ⛔ Not graded, not routed. Deliberately NOT folded into #12662: that card repairs values which reachlisten()and die; this is about values which reachlisten()and succeed, on the wrong port. Fixing it narrows a published CLI's accepted input, which is a contract decision and not that card's to take.Measured
os servereads its port withparseInt(flags.port)— before #12662 and after it, deliberately.parseIntstops at the first character it cannot use and returns what it has:parseInt3e31e103000abc0x0BB83000.0+3000" 3000"Measured with
node -eon this checkout, Node v22.22.2, against bothparseIntand a realnet.createServer().listen().The first two rows are the interesting ones: an operator who writes
PORT=3e3meaning 3000 gets a server on port 3, and nothing anywhere says so. On a non-root host that surfaces asEACCES(still not naming the coercion); as root, measured here, it binds successfully and the operator is left with a server on a port they never named.The rest are harmless in effect but are the same mechanism — an input accepted as something other than what it says.
Why #12662 left it
#12662's ruling was that no value which boots successfully today may be refused by the new validation — a falsifiable criterion, and the reason
parseIntwas kept as the reader instead of moving toFlags.integer(whose/^-?\d+$/refuses every row above).serve-port-validation.test.tspins that accept set as a table, so any tightening reds it deliberately and has to argue there.Not established here
PORT.PORTcarries it). ⛔ Not decided here.PORT=3e3 was read as port 3) — the same shape the rest of this family took, where the behaviour stayed and only the silence was repaired ([finding]getAvailablePortreturns a DIFFERENT port than requested and nothing says so — the drift is announced nowhere, and three test files have been silently talking past it #12543, [finding] WhengetAvailablePortexhausts its 100-port search,os servesilently retries the port it already knows is busy and dies on a raw kernel error #12620).Re-check
Dedup
Searched the port family: #12662 is the only open card (this finding's parent, and it explicitly leaves this undecided); #12543, #12620, #12525, #12526, #12548, #12441, #11113, #11669 are closed and all about port conflict, contention or the arg separator. Nothing covers
parseIntcoercion of a valid-looking port.Generated by Claude Code