fix: keep commands and secrets out of ExecError messages and inspect output

This commit is contained in:
Narciso
2026-10-21 22:18:25 -04:00
parent e0c8d80cde
commit e76283bc09
3 changed files with 46 additions and 14 deletions
@@ -0,0 +1,17 @@
import { inspect } from "node:util";
import { execAsync, ExecError } from "@dokploy/server/utils/process/execAsync";
import { describe, expect, it } from "vitest";
describe("execAsync failure", () => {
it("does not leak secrets embedded in the command", async () => {
const secret = "s3cr3t-value";
const error = await execAsync(`printf %s '${secret}' >/dev/null; exit 3`)
.then(() => null)
.catch((e) => e);
expect(error).toBeInstanceOf(ExecError);
expect(error.exitCode).toBe(3);
expect(error.message).not.toContain(secret);
expect(inspect(error)).not.toContain(secret);
});
});
@@ -25,6 +25,11 @@ export class ExecError extends Error {
this.originalError = details.originalError;
this.serverId = details.serverId;
// Keep the command (may embed secrets) out of console/inspect output.
for (const key of ["command", "originalError"] as const) {
Object.defineProperty(this, key, { enumerable: false });
}
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ExecError);
+24 -14
View File
@@ -23,6 +23,10 @@ export { ExecError } from "./ExecError";
const execAsyncBase = util.promisify(exec);
// Node embeds the full command in error.message; it may carry secrets.
const withoutCommand = (message: string, command: string) =>
message.split(command).join("[command omitted]");
export const execAsync = async (
command: string,
options?: { cwd?: string; env?: NodeJS.ProcessEnv; shell?: string },
@@ -42,13 +46,16 @@ export const execAsync = async (
// @ts-expect-error
const stderr = error.stderr?.toString() || "";
throw new ExecError(`Command execution failed: ${error.message}`, {
command,
stdout,
stderr,
exitCode,
originalError: error,
});
throw new ExecError(
`Command execution failed: ${withoutCommand(error.message, command)}`,
{
command,
stdout,
stderr,
exitCode,
originalError: error,
},
);
}
throw error;
}
@@ -71,13 +78,16 @@ export const execAsyncStream = (
const childProcess = exec(command, options, (error) => {
if (error) {
reject(
new ExecError(`Command execution failed: ${error.message}`, {
command,
stdout: stdoutComplete,
stderr: stderrComplete,
exitCode: error.code,
originalError: error,
}),
new ExecError(
`Command execution failed: ${withoutCommand(error.message, command)}`,
{
command,
stdout: stdoutComplete,
stderr: stderrComplete,
exitCode: error.code,
originalError: error,
},
),
);
return;
}