From 480c1072b29fc4f90665cb52483b814b0598b059 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Tue, 18 Aug 2026 08:04:49 +0000 Subject: [PATCH 1/2] fix(registry/coder/modules/local-windows-rdp): escape special characters in credentials The password was interpolated into a PowerShell double-quoted string, where $, backtick, and " are all interpreted, so the setup script aborted with a parser error and never set the admin password. The same value went into the coder:// app URL unencoded, where it truncates at the first &. - pass credentials to the setup script as PowerShell single-quoted strings, with single quotes doubled - urlencode both credentials in the app URL - add a .tftest.hcl covering a password with backslash, quotes, backtick, dollar, ampersand, angle brackets, pipe, hash, percent and plus --- .../coder/modules/local-windows-rdp/README.md | 6 +-- .../local-windows-rdp/configure-rdp.ps1 | 4 +- .../local-windows-rdp.tftest.hcl | 42 +++++++++++++++++++ .../modules/local-windows-rdp/main.test.ts | 12 +++--- .../coder/modules/local-windows-rdp/main.tf | 12 ++++-- 5 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 registry/coder/modules/local-windows-rdp/local-windows-rdp.tftest.hcl diff --git a/registry/coder/modules/local-windows-rdp/README.md b/registry/coder/modules/local-windows-rdp/README.md index 71c668ca5..74eb488b1 100644 --- a/registry/coder/modules/local-windows-rdp/README.md +++ b/registry/coder/modules/local-windows-rdp/README.md @@ -24,7 +24,7 @@ This module enables Remote Desktop Protocol (RDP) on Windows workspaces and adds module "rdp_desktop" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/local-windows-rdp/coder" - version = "1.0.4" + version = "1.0.5" agent_id = coder_agent.main.id agent_name = "main" } @@ -57,7 +57,7 @@ Uses default credentials (Username: `Administrator`, Password: `coderRDP!`): module "rdp_desktop" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/local-windows-rdp/coder" - version = "1.0.4" + version = "1.0.5" agent_id = coder_agent.main.id agent_name = "main" } @@ -71,7 +71,7 @@ Specify a custom display name for the `coder_app` button: module "rdp_desktop" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/local-windows-rdp/coder" - version = "1.0.4" + version = "1.0.5" agent_id = coder_agent.main.id agent_name = "windows" display_name = "Windows Desktop" diff --git a/registry/coder/modules/local-windows-rdp/configure-rdp.ps1 b/registry/coder/modules/local-windows-rdp/configure-rdp.ps1 index 34a3b3ec9..bbcf8f722 100644 --- a/registry/coder/modules/local-windows-rdp/configure-rdp.ps1 +++ b/registry/coder/modules/local-windows-rdp/configure-rdp.ps1 @@ -93,8 +93,8 @@ function Start-RDPService { # Main execution try { # Template variables from Terraform - $username = "${username}" - $password = "${password}" + $username = '${username}' + $password = '${password}' # Validate inputs if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) { diff --git a/registry/coder/modules/local-windows-rdp/local-windows-rdp.tftest.hcl b/registry/coder/modules/local-windows-rdp/local-windows-rdp.tftest.hcl new file mode 100644 index 000000000..784f73c11 --- /dev/null +++ b/registry/coder/modules/local-windows-rdp/local-windows-rdp.tftest.hcl @@ -0,0 +1,42 @@ +run "plan_with_defaults" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "main" + } + + assert { + condition = strcontains(resource.coder_script.rdp_setup.script, "$password = 'coderRDP!'") + error_message = "The default password should use a PowerShell single-quoted string." + } + + assert { + condition = endswith(resource.coder_app.rdp_desktop.url, "username=Administrator&password=coderRDP%21") + error_message = "The default app URL should encode its credentials." + } +} + +run "plan_with_special_characters" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "main" + password = "N;JVO*U\\mL^a*P\"'`$&<>|#%+" + } + + # A PowerShell single-quoted string is literal except for the single quote, + # which is escaped by doubling it. + assert { + condition = strcontains(resource.coder_script.rdp_setup.script, format("$password = '%s'", replace(var.password, "'", "''"))) + error_message = "The setup script should preserve special characters in the password." + } + + # Without encoding, the password truncates at the first & and drops + # everything after a #. + assert { + condition = endswith(resource.coder_app.rdp_desktop.url, "username=${urlencode(var.username)}&password=${urlencode(var.password)}") + error_message = "The app URL should encode each credential parameter." + } +} diff --git a/registry/coder/modules/local-windows-rdp/main.test.ts b/registry/coder/modules/local-windows-rdp/main.test.ts index 9799b481d..1fc696cc5 100644 --- a/registry/coder/modules/local-windows-rdp/main.test.ts +++ b/registry/coder/modules/local-windows-rdp/main.test.ts @@ -81,7 +81,8 @@ describe("local-windows-rdp", async () => { expect(app?.url).toContain("/v0/open/ws/"); expect(app?.url).toContain("/agent/main/rdp"); expect(app?.url).toContain("username=Administrator"); - expect(app?.url).toContain("password=coderRDP!"); + // Credentials are URL-encoded so characters like & and # survive. + expect(app?.url).toContain("password=coderRDP%21"); }); it("should create RDP configuration script", async () => { @@ -125,7 +126,7 @@ describe("local-windows-rdp", async () => { // Verify custom credentials in URI expect(app?.url).toContain("/agent/windows-agent/rdp"); expect(app?.url).toContain("username=CustomUser"); - expect(app?.url).toContain("password=CustomPass123!"); + expect(app?.url).toContain("password=CustomPass123%21"); }); it("should pass custom credentials to PowerShell script", async () => { @@ -139,8 +140,9 @@ describe("local-windows-rdp", async () => { const script = findRdpScript(state); // Verify custom credentials are in the script - expect(script?.script).toContain('$username = "TestAdmin"'); - expect(script?.script).toContain('$password = "TestPassword123!"'); + // PowerShell single-quoted strings keep every character literal. + expect(script?.script).toContain("$username = 'TestAdmin'"); + expect(script?.script).toContain("$password = 'TestPassword123!'"); }); it("should handle sensitive password variable", async () => { @@ -153,7 +155,7 @@ describe("local-windows-rdp", async () => { const app = findRdpApp(state); // Verify password is included in URI even when sensitive - expect(app?.url).toContain("password=SensitivePass123!"); + expect(app?.url).toContain("password=SensitivePass123%21"); }); it("should use correct default agent name", async () => { diff --git a/registry/coder/modules/local-windows-rdp/main.tf b/registry/coder/modules/local-windows-rdp/main.tf index 0999c1ffd..6c8748c6a 100644 --- a/registry/coder/modules/local-windows-rdp/main.tf +++ b/registry/coder/modules/local-windows-rdp/main.tf @@ -53,6 +53,12 @@ variable "group" { locals { # Extract server name from workspace access URL server_name = regex("https?:\\/\\/([^\\/]+)", data.coder_workspace.me.access_url)[0] + + # The setup script passes these values as PowerShell single-quoted strings, + # which are literal apart from the single quote itself. Doubling the single + # quotes keeps values containing $, backticks, or double quotes intact. + ps_username = replace(var.username, "'", "''") + ps_password = replace(var.password, "'", "''") } data "coder_workspace" "me" {} @@ -62,8 +68,8 @@ resource "coder_script" "rdp_setup" { display_name = "Configure RDP" icon = "/icon/rdp.svg" script = templatefile("${path.module}/configure-rdp.ps1", { - username = var.username - password = var.password + username = local.ps_username + password = local.ps_password }) run_on_start = true } @@ -72,7 +78,7 @@ resource "coder_app" "rdp_desktop" { agent_id = var.agent_id slug = "rdp-desktop" display_name = var.display_name - url = "coder://${local.server_name}/v0/open/ws/${data.coder_workspace.me.name}/agent/${var.agent_name}/rdp?username=${var.username}&password=${var.password}" + url = "coder://${local.server_name}/v0/open/ws/${data.coder_workspace.me.name}/agent/${var.agent_name}/rdp?username=${urlencode(var.username)}&password=${urlencode(var.password)}" icon = "/icon/rdp.svg" external = true order = var.order From 226133f7f87fbc722bc280a6dee969efeac39d25 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Tue, 18 Aug 2026 08:14:28 +0000 Subject: [PATCH 2/2] docs(registry/coder/modules/local-windows-rdp): document generated credentials Show the per-workspace random_password pattern so the credential never has to live in the template, and note that special characters now survive. --- .../coder/modules/local-windows-rdp/README.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/registry/coder/modules/local-windows-rdp/README.md b/registry/coder/modules/local-windows-rdp/README.md index 74eb488b1..d8a9993b5 100644 --- a/registry/coder/modules/local-windows-rdp/README.md +++ b/registry/coder/modules/local-windows-rdp/README.md @@ -78,3 +78,27 @@ module "rdp_desktop" { order = 1 } ``` + +### Generated credentials + +Rather than relying on the shared default password, generate one per workspace +so the credential never lives in the template: + +```tf +resource "random_password" "rdp" { + length = 24 + special = true +} + +module "rdp_desktop" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/local-windows-rdp/coder" + version = "1.0.5" + agent_id = coder_agent.main.id + agent_name = "main" + password = random_password.rdp.result +} +``` + +Generated passwords are passed through verbatim, including characters such as +`\`, `"`, `'`, `` ` ``, `$`, `&`, and `#`.