From bfc2aefdb8ab92b7284de7f9e485a5504502d944 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 30 Mar 2026 19:59:08 +0000 Subject: [PATCH] fix: smarter secret masking based on secret length (#8629) Co-authored-by: Claude Opus 4.6 (1M context) --- .../tests/sensitive_log_masking.rs | 16 ++++++++-------- .../windmill-common/src/sensitive_log_masks.rs | 12 ++++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/backend/windmill-api-integration-tests/tests/sensitive_log_masking.rs b/backend/windmill-api-integration-tests/tests/sensitive_log_masking.rs index fb759ee4b5..d174f3759b 100644 --- a/backend/windmill-api-integration-tests/tests/sensitive_log_masking.rs +++ b/backend/windmill-api-integration-tests/tests/sensitive_log_masking.rs @@ -215,8 +215,8 @@ export async function main() { "scenario 1: secret value leaked in logs\nLogs:\n{logs1}" ); assert!( - logs1.contains("The secret value is: alp*****"), - "scenario 1: expected masked output with first 3 chars\nLogs:\n{logs1}" + logs1.contains("The secret value is: alp*****k2m"), + "scenario 1: expected masked output with first 3 + last 3 chars\nLogs:\n{logs1}" ); assert!( logs1.contains("[windmill] secret value was masked for security reasons, use string transformations to display full value"), @@ -277,11 +277,11 @@ export async function main() { "scenario 3: secret2 leaked\nLogs:\n{logs3}" ); assert!( - logs3.contains("secret1=alp*****"), + logs3.contains("secret1=alp*****k2m"), "scenario 3: secret1 not masked\nLogs:\n{logs3}" ); assert!( - logs3.contains("secret2=bet*****"), + logs3.contains("secret2=bet*****n3p"), "scenario 3: secret2 not masked\nLogs:\n{logs3}" ); @@ -309,7 +309,7 @@ export async function main() { "scenario 4: secret leaked mid-string\nLogs:\n{logs4}" ); assert!( - logs4.contains("token=alp*****&user=bob&format=json"), + logs4.contains("token=alp*****k2m&user=bob&format=json"), "scenario 4: mid-string masking failed\nLogs:\n{logs4}" ); @@ -338,7 +338,7 @@ export async function main() { !logs5.contains(secret2), "scenario 5: secret leaked\nLogs:\n{logs5}" ); - let mask_count = logs5.matches("bet*****").count(); + let mask_count = logs5.matches("bet*****n3p").count(); assert!( mask_count >= 3, "scenario 5: expected >= 3 masked occurrences, found {mask_count}\nLogs:\n{logs5}" @@ -374,7 +374,7 @@ export async function main() { "scenario 6: encrypted password leaked\nLogs:\n{logs6}" ); assert!( - logs6.contains("password is: enc*****"), + logs6.contains("password is: enc*****q5r"), "scenario 6: encrypted password not masked\nLogs:\n{logs6}" ); @@ -403,7 +403,7 @@ export async function main() { "scenario 7: resource secret leaked\nLogs:\n{logs7}" ); assert!( - logs7.contains("db password: res*****"), + logs7.contains("db password: res*****7t2"), "scenario 7: resource secret not masked\nLogs:\n{logs7}" ); // Non-secret field should remain visible diff --git a/backend/windmill-common/src/sensitive_log_masks.rs b/backend/windmill-common/src/sensitive_log_masks.rs index 8123d6d55b..b6f6262b77 100644 --- a/backend/windmill-common/src/sensitive_log_masks.rs +++ b/backend/windmill-common/src/sensitive_log_masks.rs @@ -88,8 +88,16 @@ pub fn snapshot(job_id: &Uuid) -> Option { let replacements: Vec = sorted .iter() .map(|s| { - let prefix: String = s.chars().take(3).collect(); - format!("{}*****", prefix) + let char_count = s.chars().count(); + if char_count > 20 { + let prefix: String = s.chars().take(3).collect(); + let suffix: String = s.chars().skip(char_count - 3).collect(); + format!("{}*****{}", prefix, suffix) + } else { + let first: String = s.chars().take(1).collect(); + let last: String = s.chars().skip(char_count - 1).collect(); + format!("{}*****{}", first, last) + } }) .collect();