From 9b6fa3c98acc747f9be38d55c60017d2cfbb8556 Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Thu, 19 Feb 2026 21:46:20 +0000 Subject: [PATCH] fix: escape single quotes and backticks in vault secret values (#394) The vault.get() regex accepts single quotes, double quotes, and backticks as argument delimiters, but the escaping logic only handled double quotes (plus backslashes and whitespace). Add escaping for single quotes and backticks as defense-in-depth against future quoting changes. Co-Authored-By: Magistr <2269529+umag@users.noreply.github.com> --- src/domain/expressions/model_resolver.ts | 2 ++ src/domain/vaults/vault_expression_test.ts | 26 +++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/domain/expressions/model_resolver.ts b/src/domain/expressions/model_resolver.ts index 34ee758..50de807 100644 --- a/src/domain/expressions/model_resolver.ts +++ b/src/domain/expressions/model_resolver.ts @@ -775,6 +775,8 @@ export class ModelResolver { const escapedValue = secretValue .replace(/\\/g, "\\\\") .replace(/"/g, '\\"') + .replace(/'/g, "\\'") + .replace(/`/g, "\\`") .replace(/\n/g, "\\n") .replace(/\r/g, "\\r") .replace(/\t/g, "\\t"); diff --git a/src/domain/vaults/vault_expression_test.ts b/src/domain/vaults/vault_expression_test.ts index 29b1c7a..a8634c9 100644 --- a/src/domain/vaults/vault_expression_test.ts +++ b/src/domain/vaults/vault_expression_test.ts @@ -182,6 +182,26 @@ Deno.test("ModelResolver.resolveVaultExpressions", async (t) => { assertEquals(result, '"col1\\tcol2\\tcol3"'); }); + await t.step("should escape single quotes in secret values", async () => { + const resolver = createResolverWithMockVault({ + "quote-secret": "p@ss'w0rd", + }); + const result = await resolver.resolveVaultExpressions( + "vault.get(test-vault, quote-secret)", + ); + assertEquals(result, '"p@ss\\\'w0rd"'); + }); + + await t.step("should escape backticks in secret values", async () => { + const resolver = createResolverWithMockVault({ + "backtick-secret": "value`with`backticks", + }); + const result = await resolver.resolveVaultExpressions( + "vault.get(test-vault, backtick-secret)", + ); + assertEquals(result, '"value\\`with\\`backticks"'); + }); + await t.step( "should handle complex secret with multiple special characters", async () => { @@ -256,7 +276,7 @@ Deno.test("ModelResolver.resolveVaultExpressions", async (t) => { const result = await resolver.resolveVaultExpressions( "vault.get(test-vault, dollar-backtick)", ); - assertEquals(result, '"prefix$`suffix"'); + assertEquals(result, '"prefix$\\`suffix"'); }, ); @@ -269,7 +289,7 @@ Deno.test("ModelResolver.resolveVaultExpressions", async (t) => { const result = await resolver.resolveVaultExpressions( "vault.get(test-vault, dollar-quote)", ); - assertEquals(result, '"prefix$\'suffix"'); + assertEquals(result, '"prefix$\\\'suffix"'); }, ); @@ -308,7 +328,7 @@ Deno.test("ModelResolver.resolveVaultExpressions", async (t) => { const result = await resolver.resolveVaultExpressions( "vault.get(test-vault, multi-dollar)", ); - assertEquals(result, '"a]$&b$`c$\'d$$e$1f"'); + assertEquals(result, '"a]$&b$\\`c$\\\'d$$e$1f"'); }, );