From 6215c7f159dfaa3b57f78b7e021932c0c14a80e0 Mon Sep 17 00:00:00 2001 From: Souvik Kumar Date: Sun, 27 Sep 2026 00:23:23 +0530 Subject: [PATCH] fix: redact S3 credentials in backup logs regardless of shell quoting --- .../backups/redact-credentials.test.ts | 47 +++++++++++++++++++ packages/server/src/utils/backups/redact.ts | 18 +++++-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/__test__/backups/redact-credentials.test.ts b/apps/dokploy/__test__/backups/redact-credentials.test.ts index 5fff508cc..f5e3e01e0 100644 --- a/apps/dokploy/__test__/backups/redact-credentials.test.ts +++ b/apps/dokploy/__test__/backups/redact-credentials.test.ts @@ -1,4 +1,6 @@ +import type { Destination } from "@dokploy/server/services/destination"; import { redactRcloneCredentials } from "@dokploy/server/utils/backups/redact"; +import { getS3Credentials } from "@dokploy/server/utils/backups/utils"; import { describe, expect, it } from "vitest"; describe("redactRcloneCredentials (#4621)", () => { @@ -48,3 +50,48 @@ describe("redactRcloneCredentials (#4621)", () => { expect(redacted).toContain("[REDACTED]"); }); }); + +describe("redactRcloneCredentials with getS3Credentials output (#5519)", () => { + const buildCommand = (accessKey: string, secretAccessKey: string) => + `rclone rcat ${getS3Credentials({ + accessKey, + secretAccessKey, + region: "us-west-001", + endpoint: "https://s3.us-west-001.backblazeb2.com", + provider: "Other", + } as Destination).join(" ")} :s3:bucket/file.gz`; + + it.each([ + [ + "plain alphanumeric", + "001aaaabbbbccccdd0000000001", + "K001FAKEfakeFAKEfake", + ], + [ + "containing slashes", + "AKIAIOSFODNN7EXAMPLE", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + ], + ["containing spaces", "key with space", "secret with space"], + ["containing quotes", "it's-a-key", `say "hi" it's`], + ["containing shell metacharacters", "key$HOME", "sec;ret`id`&|\\x"], + ])("redacts credentials %s", (_, accessKey, secretAccessKey) => { + const redacted = redactRcloneCredentials( + buildCommand(accessKey, secretAccessKey), + ); + expect(redacted).toContain( + '--s3-access-key-id="[REDACTED]" --s3-secret-access-key="[REDACTED]" --s3-region=us-west-001', + ); + expect(redacted).not.toContain(accessKey); + expect(redacted).not.toContain(secretAccessKey); + expect(redacted).toContain(":s3:bucket/file.gz"); + }); + + it("redacts credentials embedded in an error string", () => { + const errorStr = `Error: Command failed: ${buildCommand("MYKEY", "MY/SECRET")}`; + const redacted = redactRcloneCredentials(errorStr); + expect(redacted).not.toContain("MYKEY"); + expect(redacted).not.toContain("MY/SECRET"); + expect(redacted).toContain("[REDACTED]"); + }); +}); diff --git a/packages/server/src/utils/backups/redact.ts b/packages/server/src/utils/backups/redact.ts index 065e76d8c..b4787dced 100644 --- a/packages/server/src/utils/backups/redact.ts +++ b/packages/server/src/utils/backups/redact.ts @@ -1,12 +1,24 @@ +// A shell word as produced by shell-quote: bare, 'single' or "double" quoted. +const SHELL_WORD = String.raw`(?:[^\s'"\\]|\\.|'[^']*'|"(?:[^"\\]|\\.)*")+`; + +const ACCESS_KEY_PATTERN = new RegExp( + `(--s3-access-key-id=)${SHELL_WORD}`, + "g", +); +const SECRET_KEY_PATTERN = new RegExp( + `(--s3-secret-access-key=)${SHELL_WORD}`, + "g", +); + /** * Redacts S3 credentials from rclone command strings. * * Used to prevent credential leakage in structured logs and error output. * Matches the flag format produced by `getS3Credentials()`: - * --s3-access-key-id="VALUE" and --s3-secret-access-key="VALUE" + * --s3-access-key-id=VALUE and --s3-secret-access-key=VALUE (shell-quoted) */ export const redactRcloneCredentials = (command: string): string => { return command - .replace(/(--s3-access-key-id=)"[^"]*"/g, '$1"[REDACTED]"') - .replace(/(--s3-secret-access-key=)"[^"]*"/g, '$1"[REDACTED]"'); + .replace(ACCESS_KEY_PATTERN, '$1"[REDACTED]"') + .replace(SECRET_KEY_PATTERN, '$1"[REDACTED]"'); };