Skip to content

Commit 07771ca

Browse files
author
dirkwa
committed
fix(streams): validate provider config interpolated into native gateway commands
Execute runs through a shell: hosts, devices and interfaces are now checked against the character set their legitimate values need, and ports, baud rates and NAME fields must be finite numbers — a malformed value fails the provider loudly instead of reaching sh -c (or turning into '-u NaN'). The receive-only ydwg02-udp bridge also stops wiring stdin, so the admin UI no longer reports transmit activity on a connection that cannot transmit.
1 parent 8229be1 commit 07771ca

1 file changed

Lines changed: 35 additions & 13 deletions

File tree

packages/streams/src/simple.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,31 @@ function nmea2000input(
401401
// PLAIN/FAST CSV on stdout, which the downstream N2kAnalyzer decodes.
402402
// Where toChildProcess is set, the same CSV format is accepted on stdin
403403
// for outbound PGNs (encoded by canboatjs in execute.ts).
404+
//
405+
// Execute runs the command through a shell, so every provider-config
406+
// value is validated before interpolation: hosts/devices/interfaces
407+
// against the character set their legitimate values need, numbers as
408+
// finite. Anything else fails the provider loudly.
409+
const safeArg = (value: unknown, what: string): string => {
410+
const s = String(value ?? '')
411+
if (!/^[A-Za-z0-9_.:/-]+$/.test(s)) {
412+
throw new Error(`invalid ${what} for NMEA 2000 connection: '${s}'`)
413+
}
414+
return s
415+
}
416+
const safeNum = (value: unknown, what: string): number => {
417+
const n = Number(value)
418+
if (!Number.isFinite(n)) {
419+
throw new Error(
420+
`invalid ${what} for NMEA 2000 connection: '${String(value)}'`
421+
)
422+
}
423+
return n
424+
}
404425
let command: string
405426
let toChildProcess: string | undefined
406427
if (subOptions.type === 'ngt-1') {
407-
command = `actisense-serial -s ${subOptions.baudrate ?? 115200} ${subOptions.device}`
428+
command = `actisense-serial -s ${safeNum(subOptions.baudrate ?? 115200, 'baud rate')} ${safeArg(subOptions.device, 'device')}`
408429
toChildProcess = 'nmea2000out'
409430
} else if (subOptions.type === 'canbus') {
410431
// canboat's socketcan-serial rather than candump|candump2analyzer:
@@ -415,34 +436,35 @@ function nmea2000input(
415436
command =
416437
`socketcan-serial` +
417438
(subOptions.uniqueNumber
418-
? ` -u ${Number(subOptions.uniqueNumber)}`
439+
? ` -u ${safeNum(subOptions.uniqueNumber, 'unique number')}`
440+
: '') +
441+
(subOptions.mfgCode
442+
? ` -m ${safeNum(subOptions.mfgCode, 'manufacturer code')}`
419443
: '') +
420-
(subOptions.mfgCode ? ` -m ${Number(subOptions.mfgCode)}` : '') +
421-
` ${subOptions.interface}`
444+
` ${safeArg(subOptions.interface, 'interface')}`
422445
toChildProcess = 'nmea2000out'
423446
} else if (subOptions.type === 'ikonvert') {
424-
command = `ikonvert-serial -s ${subOptions.baudrate ?? 230400} ${subOptions.device}`
447+
command = `ikonvert-serial -s ${safeNum(subOptions.baudrate ?? 230400, 'baud rate')} ${safeArg(subOptions.device, 'device')}`
425448
toChildProcess = 'nmea2000out'
426449
} else if (subOptions.type === 'maretron-ipg') {
427450
const password = (subOptions as { password?: string }).password
428451
command =
429452
`maretron-ipg` +
430453
(password ? ` --password=${shellescape(password)}` : '') +
431-
` tcp://${subOptions.host}:${subOptions.port ?? 6543}`
454+
` tcp://${safeArg(subOptions.host, 'host')}:${safeNum(subOptions.port ?? 6543, 'port')}`
432455
toChildProcess = 'nmea2000out'
433456
} else if (subOptions.type === 'ydwg02') {
434-
command = `canboat interface --kind ydwg tcp://${subOptions.host}:${subOptions.port ?? 1457}`
457+
command = `canboat interface --kind ydwg tcp://${safeArg(subOptions.host, 'host')}:${safeNum(subOptions.port ?? 1457, 'port')}`
435458
toChildProcess = 'nmea2000out'
436459
} else if (subOptions.type === 'ydwg02-udp') {
437-
// The YDWG-02's UDP mode is receive-only; the bridge drops
438-
// outbound frames with a warning.
439-
command = `canboat interface --kind ydwg udp://${subOptions.port ?? 1457}`
440-
toChildProcess = 'nmea2000out'
460+
// The YDWG-02's UDP mode is receive-only — no stdin wiring, so
461+
// the admin UI never reports transmit activity for it.
462+
command = `canboat interface --kind ydwg udp://${safeNum(subOptions.port ?? 1457, 'port')}`
441463
} else if (subOptions.type === 'navlink2') {
442-
command = `canboat interface --kind ikonvert tcp://${subOptions.host}:${subOptions.port ?? 6001}`
464+
command = `canboat interface --kind ikonvert tcp://${safeArg(subOptions.host, 'host')}:${safeNum(subOptions.port ?? 6001, 'port')}`
443465
toChildProcess = 'nmea2000out'
444466
} else if (subOptions.type === 'w2k-1-ascii') {
445-
command = `canboat interface --kind w2k-ascii tcp://${subOptions.host}:${subOptions.port ?? 60002}`
467+
command = `canboat interface --kind w2k-ascii tcp://${safeArg(subOptions.host, 'host')}:${safeNum(subOptions.port ?? 60002, 'port')}`
446468
toChildProcess = 'nmea2000out'
447469
} else {
448470
throw new Error(`unknown NMEA2000 type ${subOptions.type}`)

0 commit comments

Comments
 (0)