Skip to content

Map keys containing quotes, backslashes, ${...}, or %{...} are not escaped, producing invalid Terraform #341

Description

@sakul-learning

Summary

TFExpression.resolveExpressionPart() renders tokenized and plain object maps by interpolating object keys directly into double-quoted strings in the generated Terraform expression output — without any escaping. A map key containing a double quote ("), backslash (\), ${...}, or %{...} sequence will either produce syntactically invalid Terraform or silently change the key into a Terraform interpolation/template directive.

Two rendering paths are affected:

  • Expression rendering (packages/cdktn/src/tfExpression.ts lines 54–60 and 69–73) — inside ${ … } expression bodies
  • HCL map rendering (packages/cdktn/src/hcl/render.ts line 22) — for static HCL maps

Affected code

Expression rendering — tfExpression.ts

// Line 54-59: Tokenized (resolvable) object
if (typeof resolvedArg === "object") {
  return `{${Object.keys(resolvedArg)
    .map(
      (key) => `"${key}" = ${this.resolveArg(context, resolvedArg[key])}`,  // ❌ key is raw
    )
    .join(", ")}}`;
}

// Line 69-72: Plain object (same issue)
if (typeof resolvedArg === "object" && resolvedArg !== null) {
  return `{${Object.keys(resolvedArg)
    .map((key) => `"${key}" = ${this.resolveArg(context, arg[key])}`)  // ❌ key is raw
    .join(", ")}}`;
}

The existing escapeString() method (lines 108–112) only handles \n and ${:

protected escapeString(str: string) {
  return str
    .replace(/\n/g, "\\n")
    .replace(/\${/g, "$${");
}

…but it is never called on map keys in either object-rendering path.

HCL map rendering — hcl/render.ts

function wrapIdentifierInQuotesIfNeeded(key: string): string {
  return /(^\d)|[^A-Za-z0-9_-]/.test(key) ? `"${key}"` : key;  // ❌ key is raw
}

The HCL renderer has escapeQuotes() (line 10–16) for values, but it is not applied to map keys.

Reproduction

Any user code that passes an object literal or tokenized map containing a problematic key into a Terraform expression triggers the bug.

import { Fn } from "cdktf";

// Double quote in key — produces broken HCL
Fn.keys({ keywithquotes: "value" });
// Renders to: ${keys({"key"with"quotes" = "value"})}   ← ❌ unterminated string

// Backslash in key — can escape the closing quote
Fn.keys({ "key\\broken": "value" });
// Renders to: ${keys({"key\broken" = "value"})}         ← ❌ ambiguous escape

// ${...} in key — opens a Terraform interpolation
Fn.keys({ "${var.secret}": "value" });
// Renders to: ${keys({"${var.secret}" = "value"})}      ← ❌ injected interpolation

// %{...} in key — opens a Terraform template directive
Fn.keys({ "%{ if true }injected%{ endif }": "value" });
// Renders to: ${keys({"%{ if true }injected%{ endif }" = "value"})}  ← ❌ template directive

Suggested fix

  1. Extend escapeString() in TFExpression to also escape ", \, %{, and control characters (backslash must be escaped first, before quote escaping introduces its own backslash)
  2. Call escapeString() on map keys in both object-rendering paths in resolveExpressionPart()
  3. Apply the same fix to wrapIdentifierInQuotesIfNeeded() in hcl/render.ts

Impact

  • Corruption: keys with quotes/backslashes produce syntactically invalid HCL, causing terraform plan/apply to fail at parse time
  • Security concern: keys containing ${...} or %{...} could be interpreted as Terraform interpolations or template directives, potentially allowing injection through user-controlled map keys

Flagged during review of PR #296 by @so0k.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions