Skip to content

Commit 8de85cf

Browse files
authored
Escape the config path in the generated unattend.xml (#64)
Generate-UnattendXML XML-escaped the computer name and the local admin name but interpolated the embedded Winnow config path raw. A legal path containing &, < or > produced a malformed autounattend.xml that Windows Setup silently rejects, so first-boot configuration would just not run. Escape it the same way as the other two user-supplied values.
1 parent bc05a72 commit 8de85cf

5 files changed

Lines changed: 66 additions & 2 deletions

File tree

‎.github/workflows/test.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,15 @@ jobs:
149149
exit 1
150150
}
151151
152+
- name: Run unattend generator tests
153+
shell: pwsh
154+
run: |
155+
$result = Invoke-Pester -Path "Tests/Unit/Test-UnattendGenerator.ps1" -Output Detailed -PassThru
156+
if ($result.FailedCount -gt 0) {
157+
Write-Error "$($result.FailedCount) test(s) failed in Test-UnattendGenerator.ps1"
158+
exit 1
159+
}
160+
152161
- name: Run integration tests
153162
shell: pwsh
154163
run: |

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Document all notable Winnow changes in this file. Releases before 4.0.0 were pub
44

55
Follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [Unreleased]
8+
9+
### Fixed
10+
11+
- Escape the embedded Winnow config path in the generated `autounattend.xml`. `ComputerName` and the local admin name were XML-escaped, but the config path was interpolated raw, so a legal path containing `&`, `<`, or `>` produced a malformed unattend file that Windows Setup silently rejects. All three user-supplied values are now escaped.
12+
713
## [4.2.1] - 2026-09-18
814

915
### Fixed

‎Scripts/Features/UnattendGenerator.ps1‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,13 @@ function Generate-UnattendXML {
4444
} else { "" }
4545

4646
$winSwiftFirstBoot = if (-not [string]::IsNullOrWhiteSpace($WinnowConfigPath)) {
47+
# Escape the path for XML like the other user-supplied values, so a legal path containing
48+
# &, < or > does not produce an autounattend.xml that Windows Setup silently rejects.
49+
$escapedConfigPath = [System.Security.SecurityElement]::Escape($WinnowConfigPath)
4750
@"
4851
<RunSynchronousCommand wcm:action="add">
4952
<Order>2</Order>
50-
<Path>powershell.exe -NonInteractive -ExecutionPolicy Bypass -File "C:\Winnow\Winnow-Standalone.ps1" -Config "$WinnowConfigPath"</Path>
53+
<Path>powershell.exe -NonInteractive -ExecutionPolicy Bypass -File "C:\Winnow\Winnow-Standalone.ps1" -Config "$escapedConfigPath"</Path>
5154
<Description>Apply Winnow configuration on first boot</Description>
5255
</RunSynchronousCommand>
5356
"@
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' }
2+
<#
3+
.SYNOPSIS
4+
Unit tests for the unattend.xml generator, focused on XML-escaping user input.
5+
.DESCRIPTION
6+
ComputerName, LocalAdminName, and the embedded Winnow config path are all user supplied and go
7+
into the generated autounattend.xml. Any of them can legally contain & < or >, which must be
8+
XML-escaped or Windows Setup silently rejects the file.
9+
#>
10+
11+
BeforeAll {
12+
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
13+
. (Join-Path $repoRoot 'Scripts\Features\UnattendGenerator.ps1')
14+
15+
function New-UnattendXml {
16+
param([hashtable]$Parameters)
17+
$out = Join-Path $TestDrive ('unattend-' + [guid]::NewGuid().ToString('N').Substring(0, 8) + '.xml')
18+
Generate-UnattendXML @Parameters -OutputPath $out -Confirm:$false 6>$null | Out-Null
19+
return $out
20+
}
21+
}
22+
23+
Describe 'Generate-UnattendXML' {
24+
25+
It 'produces valid XML when the config path contains XML special characters' {
26+
$path = New-UnattendXml -Parameters @{ WinnowConfigPath = 'C:\Games & Config\<win>.json' }
27+
{ [xml](Get-Content -LiteralPath $path -Raw) } | Should -Not -Throw
28+
}
29+
30+
It 'escapes the embedded config path' {
31+
$path = New-UnattendXml -Parameters @{ WinnowConfigPath = 'C:\A & B\c.json' }
32+
$raw = Get-Content -LiteralPath $path -Raw
33+
$raw | Should -Match 'A &amp; B'
34+
$raw | Should -Not -Match 'A & B'
35+
}
36+
37+
It 'produces valid XML when the computer name and admin name contain special characters' {
38+
$path = New-UnattendXml -Parameters @{ ComputerName = 'PC&1'; LocalAdminName = 'Ad<min>' }
39+
{ [xml](Get-Content -LiteralPath $path -Raw) } | Should -Not -Throw
40+
}
41+
42+
It 'produces valid XML for the default (no optional inputs)' {
43+
$path = New-UnattendXml -Parameters @{}
44+
{ [xml](Get-Content -LiteralPath $path -Raw) } | Should -Not -Throw
45+
}
46+
}

‎Winnow-Standalone.ps1‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)