Dingtian: close password-less string-protocol relay-fire hole
The string protocol (UDP 60001) has no password field but can fire relays
("11" = relay 1 on), bypassing relay_pw entirely. Proven on hardware: an
unauthenticated packet opened a relay. harden() had left it enabled "for
status reads".
- #status() now reads via the authenticated binary command (relay cmd 0x00)
instead of the string protocol, so the string protocol is no longer needed.
- harden() disables the string protocol (udp2.p=255). BEST-EFFORT: firmware
V3.6J's config API silently refuses to disable udp2 (the device web UI can),
so it's not part of the blocking verify -- harden() re-checks and returns a
warning instead of throwing. After a web-UI disable, the attack is dead and
binary control/status still work (verified on hardware).
- HardenResult gains an optional `warnings[]`; the assign route surfaces them
to the admin and logs them.
- Corrected the false comment claiming relay_pw stops an attacker (it is
defence-in-depth on plaintext UDP, not a boundary).
- Thread localAddress through the driver's UDP/HTTP calls so a multi-homed
host sources device traffic from the device-facing NIC.
- Device web login (webUser/webPassword) is no longer redacted from setup
state -- it's an operational credential for the admin-only device area;
pushPassword/relayPassword stay machine-only.
Wiki: document the vuln + fix, the firmware caveat, and the out-of-band
actuation gap (the log captures host actions only; reconciliation vs. an
independent witness is the real control and is not yet built).
This commit is contained in:
@@ -32,10 +32,15 @@ interface TestBody {
|
||||
config: Record<string, string | number | boolean>;
|
||||
}
|
||||
|
||||
// Config keys that hold device secrets — never sent back to the client. Covers
|
||||
// the push Digest password, the rotated device web-UI login, and the Dingtian
|
||||
// relay password. Centralised so /state and /assign redact consistently.
|
||||
const SECRET_CONFIG_KEYS = ["pushPassword", "webPassword", "relayPassword"] as const;
|
||||
// Config keys that hold MACHINE-ONLY secrets — never sent back to the client.
|
||||
// No human ever uses these to log in: `pushPassword` is the device→backend Digest
|
||||
// secret, `relayPassword` is the binary-protocol relay_pw. They stay redacted.
|
||||
//
|
||||
// NOTE: the device web-UI login (`webUser`/`webPassword`) is deliberately NOT
|
||||
// redacted. It's an operational credential an admin needs to reach the device's
|
||||
// own web page, and the whole device-management area is admin-only — so it's
|
||||
// surfaced in the admin device view rather than hidden. See first-run-setup.md.
|
||||
const SECRET_CONFIG_KEYS = ["pushPassword", "relayPassword"] as const;
|
||||
|
||||
function redactSecrets(config: Record<string, unknown>): Record<string, unknown> {
|
||||
const out = { ...config };
|
||||
@@ -157,6 +162,9 @@ export async function setupRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
||||
|
||||
const id = randomUUID();
|
||||
const fullConfig: Record<string, unknown> = { ...config };
|
||||
// Residual-risk warnings from device hardening (shown to the admin; the
|
||||
// save still succeeds — these are "configured, but note X" advisories).
|
||||
const hardenWarnings: string[] = [];
|
||||
|
||||
let device;
|
||||
try {
|
||||
@@ -184,8 +192,14 @@ export async function setupRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
||||
}
|
||||
|
||||
if (isHardenable(device)) {
|
||||
const { secrets } = await device.harden();
|
||||
const { secrets, warnings } = await device.harden();
|
||||
Object.assign(fullConfig, secrets); // e.g. relayPassword
|
||||
// Surface residual-risk warnings (e.g. firmware that won't disable the
|
||||
// password-less string protocol) so the admin can act (web-UI step).
|
||||
for (const w of warnings ?? []) {
|
||||
app.log.warn(`harden(${driverId} ${id}): ${w}`);
|
||||
hardenWarnings.push(w);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPushConfig(device)) {
|
||||
@@ -229,7 +243,11 @@ export async function setupRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
||||
};
|
||||
await db.insert(laneDevices).values(row);
|
||||
// Don't echo device secrets back (push Digest password, web-UI login, …).
|
||||
return reply.code(201).send({ ...row, config: redactSecrets(fullConfig) });
|
||||
return reply.code(201).send({
|
||||
...row,
|
||||
config: redactSecrets(fullConfig),
|
||||
...(hardenWarnings.length ? { warnings: hardenWarnings } : {}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user