From 07e637491e65bb34ba239c3f579d39ed2cb479c1 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:36:54 +0200 Subject: [PATCH 1/2] fix(service): compare Task Scheduler Arguments after XML decode Closes #608 --- src/service.ts | 27 +++++++++++++++++++++++++-- tests/service.test.ts | 15 +++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/service.ts b/src/service.ts index a68300827..cb49f261d 100644 --- a/src/service.ts +++ b/src/service.ts @@ -1050,6 +1050,29 @@ function taskXmlOptionalValueEquals(xml: string, tag: string, expected: string): return value?.trim().toLowerCase() === expected.toLowerCase(); } +function taskXmlDecodeEntities(value: string): string { + return value + .replace(/"/g, "\"") + .replace(/'/g, "'") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&"); +} + +/** + * Exact element-text match after one-pass decoding of XML's five predefined entities. + * Task Scheduler exports element text unescaped (`"`), while we emit `"` (#608). + * Rejects zero/multiple occurrences and nested markup (no `<` inside the value). + */ +function taskXmlElementTextEquals(xml: string, tag: string, expectedDecoded: string): boolean { + if (taskXmlHasPrefixedTag(xml, tag)) return false; + const count = taskXmlElementCount(xml, tag); + if (count !== 1) return false; + const raw = new RegExp(`<${tag}(?:\\s[^>]*?)?>\\s*([^<]*?)\\s*<\\/${tag}>`, "i").exec(xml)?.[1]; + if (raw === undefined || raw.includes("<")) return false; + return taskXmlDecodeEntities(raw) === expectedDecoded; +} + /** Validate the security/lifecycle-critical fields of the registered scheduler task. */ export function windowsTaskRegistrationHealthy( xml: string, @@ -1076,8 +1099,8 @@ export function windowsTaskRegistrationHealthy( && taskXmlOptionalValueEquals(settings, "Enabled", "true") && /\s*IgnoreNew\s*<\/MultipleInstancesPolicy>/i.test(settings) && /\s*PT0S\s*<\/ExecutionTimeLimit>/i.test(settings) - && action.includes(`${taskXmlString(wscript)}`) - && action.includes(`${taskXmlString(`/b /nologo "${launcher}"`)}`); + && taskXmlElementTextEquals(action, "Command", wscript) + && taskXmlElementTextEquals(action, "Arguments", `/b /nologo "${launcher}"`); } export interface WindowsSchedulerXmlState { diff --git a/tests/service.test.ts b/tests/service.test.ts index ef6a3dafc..1be811380 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -237,6 +237,21 @@ describe("Windows service task", () => { expect(xml).not.toContain("C:\\Users\\a&b\\.opencodex\\opencodex-service.cmd"); }); + test("accepts Task Scheduler Arguments with unescaped quotes (#608)", () => { + const wscript = "C:\\Windows\\System32\\wscript.exe"; + const launcher = "C:\\Users\\Test\\.opencodex\\service-launcher.vbs"; + const xml = buildWindowsTaskXml("ignored.cmd", launcher) + .replace(/.*?<\/Command>/, `${wscript}`) + // Windows exports element text unescaped — literal " not " + .replace( + `/b /nologo "${launcher}"`, + `/b /nologo "${launcher}"`, + ); + expect(xml).toContain(`/b /nologo "${launcher}"`); + expect(xml).not.toContain("""); + expect(windowsTaskRegistrationHealthy(xml, wscript, launcher)).toBe(true); + }); + test("validates the registered scheduler action, trigger, principal, and settings", () => { const wscript = "C:\\Windows\\System32\\wscript.exe"; const launcher = "C:\\Users\\Test\\.opencodex\\service-launcher.vbs"; From 9be8ad1befa8b219de6beab88f878b62540426a7 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:45:10 +0200 Subject: [PATCH 2/2] fix(service): trim decoded Arguments before compare --- src/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service.ts b/src/service.ts index cb49f261d..2e9bdc22d 100644 --- a/src/service.ts +++ b/src/service.ts @@ -1070,7 +1070,7 @@ function taskXmlElementTextEquals(xml: string, tag: string, expectedDecoded: str if (count !== 1) return false; const raw = new RegExp(`<${tag}(?:\\s[^>]*?)?>\\s*([^<]*?)\\s*<\\/${tag}>`, "i").exec(xml)?.[1]; if (raw === undefined || raw.includes("<")) return false; - return taskXmlDecodeEntities(raw) === expectedDecoded; + return taskXmlDecodeEntities(raw).trim() === expectedDecoded.trim(); } /** Validate the security/lifecycle-critical fields of the registered scheduler task. */