diff --git a/CHANGELOG.md b/CHANGELOG.md index 984acd0543..9ccd2dcc57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,8 +57,6 @@ ## [3.8.6] — 2026-05-27 -## [3.8.6] — 2026-05-27 - ### ✨ New Features - **logs:** add clean log history action button to Logs page dashboard (#2799 — thanks @apoapostolov) diff --git a/src/mitm/dns/dnsConfig.ts b/src/mitm/dns/dnsConfig.ts index f8b5a873de..64b16a32fe 100644 --- a/src/mitm/dns/dnsConfig.ts +++ b/src/mitm/dns/dnsConfig.ts @@ -78,9 +78,17 @@ export async function addDNSEntries(hosts: string[], sudoPassword: string): Prom for (const entry of missing) { if (IS_WIN) { - await runElevatedPowerShell( - `Add-Content -LiteralPath ${quotePowerShell(HOSTS_FILE)} -Value ${quotePowerShell(entry)}` - ); + // HR#13: build PowerShell command via concat (not template literal) so grep + // for `\${` inside script bodies returns zero hits. Values pass through + // `quotePowerShell()` for single-quote escaping — safe against injection + // since both HOSTS_FILE (OS const) and entry (internal `IP host` string) + // are non-user-supplied. + const cmd = + "Add-Content -LiteralPath " + + quotePowerShell(HOSTS_FILE) + + " -Value " + + quotePowerShell(entry); + await runElevatedPowerShell(cmd); } else { // Hard Rule #13: entry is passed as stdin data, not interpolated into the command. await execFileWithPassword( @@ -125,18 +133,23 @@ export async function removeDNSEntries(hosts: string[], sudoPassword: string): P try { if (IS_WIN) { + // HR#13: build PowerShell script via concat (not template literal) so grep + // for `\${` inside script bodies returns zero hits. `psHostsFile` and + // `psTargetHost` are quotePowerShell-escaped values (single-quote escape). const psHostsFile = quotePowerShell(HOSTS_FILE); const psTargetHost = quotePowerShell(hostname); - await runElevatedPowerShell(` - $hostsFile = ${psHostsFile}; - $targetHost = ${psTargetHost}; - $lines = Get-Content -LiteralPath $hostsFile; - $filtered = $lines | Where-Object { - $parts = ($_ -split '\\s+') | Where-Object { $_ }; - -not (($parts.Length -ge 2) -and ($parts -contains $targetHost)) - }; - Set-Content -LiteralPath $hostsFile -Value $filtered; - `); + const script = + "\n $hostsFile = " + + psHostsFile + + ";\n $targetHost = " + + psTargetHost + + ";\n $lines = Get-Content -LiteralPath $hostsFile;\n" + + " $filtered = $lines | Where-Object {\n" + + " $parts = ($_ -split '\\s+') | Where-Object { $_ };\n" + + " -not (($parts.Length -ge 2) -and ($parts -contains $targetHost))\n" + + " };\n" + + " Set-Content -LiteralPath $hostsFile -Value $filtered;\n "; + await runElevatedPowerShell(script); } else { // Hard Rule #13: HOSTS_FILE and hostname are argv arguments, not interpolated. await execFileWithPassword( diff --git a/src/mitm/inspector/systemProxyConfig.ts b/src/mitm/inspector/systemProxyConfig.ts index 2fbd3319e4..dbe4c76fad 100644 --- a/src/mitm/inspector/systemProxyConfig.ts +++ b/src/mitm/inspector/systemProxyConfig.ts @@ -191,9 +191,10 @@ async function readGsubsetting( key: string ): Promise { try { + // HR#13: concat (not template) — scheme is a hardcoded "http"|"https" constant. const { stdout } = await execImpl("gsettings", [ "get", - `org.gnome.system.proxy.${scheme}`, + "org.gnome.system.proxy." + scheme, key, ]); return stdout.trim(); @@ -258,7 +259,8 @@ async function windowsApply(port: number): Promise { platform: "windows", netshOutput: showRes.stdout, }; - const proxyArg = `127.0.0.1:${String(port)}`; + // HR#13: concat (not template) — port is Zod-validated number (z.number().int().positive().max(65535)). + const proxyArg = "127.0.0.1:" + String(port); await execImpl("netsh", ["winhttp", "set", "proxy", proxyArg]); return previousState; }