Skip to content

Commit a580b6f

Browse files
Harden YAML lexical boundaries
Use YAML s-white and ordinal token checks, consume legal BOMs, preserve flow scalar folding, and disambiguate anchor and alias colons without regressing the official corpus. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 6bde23a commit a580b6f

21 files changed

Lines changed: 346 additions & 116 deletions
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
function ConvertFrom-YamlByteOrderMark {
2+
<#
3+
.SYNOPSIS
4+
Consumes byte order marks at YAML stream and explicit document boundaries.
5+
#>
6+
[CmdletBinding()]
7+
[OutputType([string])]
8+
param (
9+
[Parameter(Mandatory)]
10+
[AllowEmptyString()]
11+
[string] $Text
12+
)
13+
14+
if ($Text.IndexOf([char] 0xFEFF) -lt 0) {
15+
return $Text
16+
}
17+
18+
$builder = [System.Text.StringBuilder]::new($Text.Length)
19+
for ($index = 0; $index -lt $Text.Length; $index++) {
20+
if ($Text[$index] -cne [char] 0xFEFF) {
21+
[void] $builder.Append($Text[$index])
22+
continue
23+
}
24+
25+
$atStreamStart = $index -eq 0
26+
$atLineStart = $index -gt 0 -and $Text[$index - 1] -ceq "`n"
27+
$beforeDirective = $index + 1 -lt $Text.Length -and $Text[$index + 1] -ceq '%'
28+
$beforeDocumentStart = $false
29+
if ($index + 3 -lt $Text.Length -and
30+
$Text.Substring($index + 1, 3).Equals(
31+
'---',
32+
[System.StringComparison]::Ordinal
33+
)) {
34+
$afterMarker = $index + 4
35+
$beforeDocumentStart = $afterMarker -ge $Text.Length -or
36+
(Test-YamlWhiteSpace -Character $Text[$afterMarker]) -or
37+
$Text[$afterMarker] -ceq "`n"
38+
}
39+
40+
if ($atStreamStart -or ($atLineStart -and ($beforeDirective -or $beforeDocumentStart))) {
41+
continue
42+
}
43+
44+
$before = $Text.Substring(0, $index)
45+
$line = ([regex]::Matches($before, "`n")).Count
46+
$lastBreak = $before.LastIndexOf("`n", [System.StringComparison]::Ordinal)
47+
$column = if ($lastBreak -lt 0) { $before.Length } else { $before.Length - $lastBreak - 1 }
48+
$mark = New-YamlMark -Index $index -Line $line -Column $column
49+
throw (New-YamlException -Start $mark -End $mark -ErrorId 'YamlInvalidByteOrderMark' -Message (
50+
'A YAML byte order mark is only allowed at the start of the stream or an explicit document.'
51+
))
52+
}
53+
54+
return $builder.ToString()
55+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
function Find-YamlCommentStart {
2+
<#
3+
.SYNOPSIS
4+
Finds a comment indicator separated by YAML s-white.
5+
#>
6+
[CmdletBinding()]
7+
[OutputType([int])]
8+
param (
9+
[Parameter(Mandatory)]
10+
[AllowEmptyString()]
11+
[string] $Text
12+
)
13+
14+
for ($index = 0; $index -lt $Text.Length; $index++) {
15+
if ($Text[$index] -ceq '#' -and (
16+
$index -eq 0 -or (Test-YamlWhiteSpace -Character $Text[$index - 1])
17+
)) {
18+
return $index
19+
}
20+
}
21+
return -1
22+
}

src/functions/private/Find-YamlMappingColon.ps1

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ function Find-YamlMappingColon {
88
param (
99
[Parameter(Mandatory)]
1010
[AllowEmptyString()]
11-
[string] $Text
11+
[string] $Text,
12+
13+
[switch] $AllowAnchorFallback
1214
)
1315

1416
if ($Text.IndexOf(':') -lt 0) {
@@ -19,6 +21,7 @@ function Find-YamlMappingColon {
1921
$singleQuoted = $false
2022
$doubleQuoted = $false
2123
$atNodeStart = $true
24+
$anchorColonCandidate = -1
2225
for ($index = 0; $index -lt $Text.Length; $index++) {
2326
$character = $Text[$index]
2427
if ($doubleQuoted) {
@@ -39,20 +42,27 @@ function Find-YamlMappingColon {
3942
}
4043
continue
4144
}
42-
if ($atNodeStart -and [char]::IsWhiteSpace($character)) {
45+
if ($atNodeStart -and (Test-YamlWhiteSpace -Character $character)) {
4346
continue
4447
}
4548
if ($atNodeStart -and $character -eq '&') {
4649
$index++
47-
while ($index -lt $Text.Length -and -not [char]::IsWhiteSpace($Text[$index])) {
50+
while ($index -lt $Text.Length -and
51+
-not (Test-YamlWhiteSpace -Character $Text[$index]) -and
52+
$Text[$index] -notin @(',', '[', ']', '{', '}')) {
53+
if (Test-YamlMappingValueIndicator -Text $Text -Index $index) {
54+
$anchorColonCandidate = $index
55+
}
4856
$index++
4957
}
5058
$index--
5159
continue
5260
}
5361
if ($atNodeStart -and $character -eq '*') {
5462
$index++
55-
while ($index -lt $Text.Length -and -not [char]::IsWhiteSpace($Text[$index])) {
63+
while ($index -lt $Text.Length -and
64+
-not (Test-YamlWhiteSpace -Character $Text[$index]) -and
65+
$Text[$index] -notin @(',', '[', ']', '{', '}')) {
5666
$index++
5767
}
5868
$index--
@@ -67,7 +77,7 @@ function Find-YamlMappingColon {
6777
} else {
6878
$index++
6979
while ($index -lt $Text.Length -and
70-
-not [char]::IsWhiteSpace($Text[$index]) -and
80+
-not (Test-YamlWhiteSpace -Character $Text[$index]) -and
7181
$Text[$index] -notin @('[', ']', '{', '}', ',')) {
7282
$index++
7383
}
@@ -94,18 +104,24 @@ function Find-YamlMappingColon {
94104
':' {
95105
if ($flowDepth -eq 0) {
96106
$nextIsSeparator = $index + 1 -ge $Text.Length
97-
$nextIsSeparator = $nextIsSeparator -or [char]::IsWhiteSpace($Text[$index + 1])
107+
$nextIsSeparator = $nextIsSeparator -or
108+
(Test-YamlWhiteSpace -Character $Text[$index + 1])
98109
if ($nextIsSeparator) {
99110
return $index
100111
}
101112
}
102113
}
103114
'#' {
104-
if ($flowDepth -eq 0 -and ($index -eq 0 -or [char]::IsWhiteSpace($Text[$index - 1]))) {
115+
if ($flowDepth -eq 0 -and (
116+
$index -eq 0 -or (Test-YamlWhiteSpace -Character $Text[$index - 1])
117+
)) {
105118
return -1
106119
}
107120
}
108121
}
109122
}
123+
if ($AllowAnchorFallback) {
124+
return $anchorColonCandidate
125+
}
110126
return -1
111127
}

src/functions/private/Get-YamlContentWithoutComment.ps1

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ function Get-YamlContentWithoutComment {
1111
[string] $Text
1212
)
1313

14-
$comment = [regex]::Match($Text, '(?<!\S)#')
15-
if ($comment.Success) {
16-
return $Text.Substring(0, $comment.Index).TrimEnd()
14+
$comment = Find-YamlCommentStart -Text $Text
15+
if ($comment -ge 0) {
16+
return $Text.Substring(0, $comment).TrimEnd(' ', "`t")
1717
}
18-
return $Text.TrimEnd()
18+
return $Text.TrimEnd(' ', "`t")
1919
}

src/functions/private/New-YamlReaderContext.ps1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ function New-YamlReaderContext {
3636
[int] $MaxNumericLength
3737
)
3838

39-
Assert-YamlText -Yaml $Yaml
4039
$text = $Yaml.Replace("`r`n", "`n").Replace("`r", "`n")
40+
$text = ConvertFrom-YamlByteOrderMark -Text $text
41+
Assert-YamlText -Yaml $text
4142
$lines = [System.Text.RegularExpressions.Regex]::Split($text, "`n")
4243
$lineStarts = [int[]]::new($lines.Count)
4344
$offset = 0

src/functions/private/Read-YamlBlockKey.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ function Read-YamlBlockKey {
6565
$first = $rest[0]
6666
if ($first -in @(',', '[', ']', '{', '}', '#', '&', '*', '!', '|', '>', "'", '"', '%', '@', '`') -or
6767
($first -in @('-', '?', ':') -and (
68-
$rest.Length -gt 1 -and [char]::IsWhiteSpace($rest[1])
68+
$rest.Length -gt 1 -and (Test-YamlWhiteSpace -Character $rest[1])
6969
))) {
7070
throw (New-YamlException -Start $start -End $start -ErrorId 'YamlInvalidPlainScalar' -Message (
7171
'The first character is not allowed in a YAML plain scalar.'
@@ -82,7 +82,7 @@ function Read-YamlBlockKey {
8282
$node = New-YamlSyntaxNode -Context $Context -Kind Scalar -Depth $Depth -Start $start -End $end
8383
Set-YamlParsedNodeProperty -Node $node -Tag $properties.Tag `
8484
-HasUnknownTag $properties.HasUnknownTag -Anchor $properties.Anchor -Context $Context
85-
$node.Value = $rest.TrimEnd()
85+
$node.Value = $rest.TrimEnd(' ', "`t")
8686
$node.Style = 'Plain'
8787
$node.IsPlainImplicit = [string]::IsNullOrEmpty($properties.Tag) -and
8888
-not $properties.HasUnknownTag

src/functions/private/Read-YamlBlockMapping.ps1

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ function Read-YamlBlockMapping {
7777
$isExplicit = Test-YamlIndicator -Text $content -Indicator '?'
7878
if ($isExplicit) {
7979
$afterQuestion = $content.Substring(1)
80-
$leading = $afterQuestion.Length - $afterQuestion.TrimStart().Length
81-
$keyText = $afterQuestion.TrimStart()
80+
$leading = $afterQuestion.Length - $afterQuestion.TrimStart(' ', "`t").Length
81+
$keyText = $afterQuestion.TrimStart(' ', "`t")
8282
$keyColumn = $contentColumn + 1 + $leading
83-
if ([string]::IsNullOrWhiteSpace((Get-YamlContentWithoutComment -Text $keyText))) {
83+
if ([string]::IsNullOrEmpty((Get-YamlContentWithoutComment -Text $keyText))) {
8484
$Context.LineIndex++
8585
Skip-YamlBlockTrivia -Context $Context
8686
if ($Context.LineIndex -ge $Context.Lines.Count) {
@@ -104,9 +104,12 @@ function Read-YamlBlockMapping {
104104
}
105105
}
106106
} elseif (Test-YamlIndicator -Text $keyText -Indicator '-') {
107-
$firstItem = $keyText.Substring(1).TrimStart()
107+
$firstItem = $keyText.Substring(1).TrimStart(' ', "`t")
108108
$dashColumn = $keyColumn
109-
$itemColumn = $dashColumn + 1 + ($keyText.Substring(1).Length - $keyText.Substring(1).TrimStart().Length)
109+
$itemColumn = $dashColumn + 1 + (
110+
$keyText.Substring(1).Length -
111+
$keyText.Substring(1).TrimStart(' ', "`t").Length
112+
)
110113
$key = Read-YamlBlockSequence -Context $Context -Indent $dashColumn -Depth ($Depth + 1) `
111114
-FirstItemText $firstItem -FirstItemColumn $itemColumn
112115
} else {
@@ -130,10 +133,10 @@ function Read-YamlBlockMapping {
130133
if ($valueIndent -eq $Indent -and
131134
(Test-YamlIndicator -Text $valueContent -Indicator ':')) {
132135
$valueText = $valueContent.Substring(1)
133-
$leading = $valueText.Length - $valueText.TrimStart().Length
134-
$valueText = $valueText.TrimStart()
136+
$leading = $valueText.Length - $valueText.TrimStart(' ', "`t").Length
137+
$valueText = $valueText.TrimStart(' ', "`t")
135138
$valueColumn = $Indent + 1 + $leading
136-
if ([string]::IsNullOrWhiteSpace((Get-YamlContentWithoutComment -Text $valueText))) {
139+
if ([string]::IsNullOrEmpty((Get-YamlContentWithoutComment -Text $valueText))) {
137140
$valueLineNumber = $Context.LineIndex
138141
$Context.LineIndex++
139142
Skip-YamlBlockTrivia -Context $Context
@@ -171,7 +174,7 @@ function Read-YamlBlockMapping {
171174
continue
172175
}
173176

174-
$colon = Find-YamlMappingColon -Text $content
177+
$colon = Find-YamlMappingColon -Text $content -AllowAnchorFallback
175178
if ($colon -lt 0) {
176179
$mark = New-YamlMark -Index ($Context.LineStarts[$lineNumber] + $contentColumn) `
177180
-Line $lineNumber -Column $contentColumn
@@ -192,17 +195,17 @@ function Read-YamlBlockMapping {
192195
-Line $lineNumber -Column $contentColumn
193196
$key = New-YamlEmptyScalar -Context $Context -Depth ($Depth + 1) -Mark $mark
194197
} else {
195-
$keyText = $content.Substring(0, $colon).TrimEnd()
198+
$keyText = $content.Substring(0, $colon).TrimEnd(' ', "`t")
196199
$key = Read-YamlBlockKey -Context $Context -Text $keyText -Line $lineNumber `
197200
-Column $contentColumn -Depth ($Depth + 1)
198201
}
199202

200203
$valueStart = $colon + 1
201204
$valueSource = $content.Substring($valueStart)
202-
$leading = $valueSource.Length - $valueSource.TrimStart().Length
203-
$valueText = $valueSource.TrimStart()
205+
$leading = $valueSource.Length - $valueSource.TrimStart(' ', "`t").Length
206+
$valueText = $valueSource.TrimStart(' ', "`t")
204207
$valueColumn = $contentColumn + $valueStart + $leading
205-
if ([string]::IsNullOrWhiteSpace((Get-YamlContentWithoutComment -Text $valueText))) {
208+
if ([string]::IsNullOrEmpty((Get-YamlContentWithoutComment -Text $valueText))) {
206209
$Context.LineIndex = $lineNumber + 1
207210
Skip-YamlBlockTrivia -Context $Context
208211
if ($Context.LineIndex -lt $Context.Lines.Count) {
@@ -242,7 +245,8 @@ function Read-YamlBlockMapping {
242245
}
243246
$Context.LineIndex = $lineNumber
244247
$value = Read-YamlBlockNode -Context $Context -ParentIndent $Indent -Depth ($Depth + 1) `
245-
-Segment $valueText -SegmentColumn $valueColumn -AllowIndentlessSequence
248+
-Segment $valueText -SegmentColumn $valueColumn -AllowIndentlessSequence `
249+
-DisallowCompactMapping
246250
}
247251
$node.Entries.Add([pscustomobject]@{ Key = $key; Value = $value })
248252
}

src/functions/private/Read-YamlBlockNode.ps1

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ function Read-YamlBlockNode {
2929
[AllowEmptyString()]
3030
[string] $PendingAnchor = '',
3131

32-
[switch] $AllowIndentlessSequence
32+
[switch] $AllowIndentlessSequence,
33+
34+
[switch] $DisallowCompactMapping
3335
)
3436

3537
$hasSegment = $PSBoundParameters.ContainsKey('Segment')
@@ -67,8 +69,9 @@ function Read-YamlBlockNode {
6769
if (
6870
(-not [string]::IsNullOrEmpty($PendingTag) -or $PendingUnknownTag -or
6971
-not [string]::IsNullOrEmpty($PendingAnchor)) -and
72+
-not $DisallowCompactMapping -and
7073
-not (Test-YamlIndicator -Text $Segment -Indicator '-') -and
71-
((Find-YamlMappingColon -Text $Segment) -ge 0 -or
74+
((Find-YamlMappingColon -Text $Segment -AllowAnchorFallback) -ge 0 -or
7275
(Test-YamlIndicator -Text $Segment -Indicator '?'))
7376
) {
7477
return Read-YamlBlockMapping -Context $Context -Indent $SegmentColumn -Depth $Depth `
@@ -78,8 +81,9 @@ function Read-YamlBlockNode {
7881
if (
7982
[string]::IsNullOrEmpty($PendingTag) -and -not $PendingUnknownTag -and
8083
[string]::IsNullOrEmpty($PendingAnchor) -and
84+
-not $DisallowCompactMapping -and
8185
-not (Test-YamlIndicator -Text $Segment -Indicator '-') -and
82-
((Find-YamlMappingColon -Text $Segment) -ge 0 -or
86+
((Find-YamlMappingColon -Text $Segment -AllowAnchorFallback) -ge 0 -or
8387
(Test-YamlIndicator -Text $Segment -Indicator '?'))
8488
) {
8589
return Read-YamlBlockMapping -Context $Context -Indent $SegmentColumn -Depth $Depth `
@@ -111,7 +115,7 @@ function Read-YamlBlockNode {
111115
$rest = Get-YamlContentWithoutComment -Text $restSource
112116
$contentColumn = $SegmentColumn + $properties.Consumed
113117

114-
if ([string]::IsNullOrWhiteSpace($rest)) {
118+
if ([string]::IsNullOrEmpty($rest)) {
115119
$Context.LineIndex++
116120
Skip-YamlBlockTrivia -Context $Context
117121
if ($Context.LineIndex -ge $Context.Lines.Count -or
@@ -154,23 +158,24 @@ function Read-YamlBlockNode {
154158
}
155159
$firstSource = $rest.Substring(1)
156160
if ($firstSource.IndexOf("`t", [System.StringComparison]::Ordinal) -ge 0 -and
157-
$firstSource.Trim() -eq '-') {
161+
$firstSource.Trim(' ', "`t") -ceq '-') {
158162
$mark = New-YamlMark -Index ($Context.LineStarts[$lineNumber] + $contentColumn + 1) `
159163
-Line $lineNumber -Column ($contentColumn + 1)
160164
throw (New-YamlException -Start $mark -End $mark -ErrorId 'YamlInvalidIndentation' -Message (
161165
'Tabs cannot separate adjacent block sequence indicators.'
162166
))
163167
}
164168
$first = $firstSource
165-
$leading = $first.Length - $first.TrimStart().Length
166-
$first = $first.TrimStart()
169+
$leading = $first.Length - $first.TrimStart(' ', "`t").Length
170+
$first = $first.TrimStart(' ', "`t")
167171
return Read-YamlBlockSequence -Context $Context -Indent $contentColumn -Depth $Depth -Tag $tag `
168172
-HasUnknownTag $unknownTag -Anchor $anchor -FirstItemText $first `
169173
-FirstItemColumn ($contentColumn + 1 + $leading)
170174
}
171175

172-
if ((Find-YamlMappingColon -Text $rest) -ge 0 -or
173-
(Test-YamlIndicator -Text $rest -Indicator '?')) {
176+
if (-not $DisallowCompactMapping -and (
177+
(Find-YamlMappingColon -Text $rest -AllowAnchorFallback) -ge 0 -or
178+
(Test-YamlIndicator -Text $rest -Indicator '?'))) {
174179
return Read-YamlBlockMapping -Context $Context -Indent $contentColumn -Depth $Depth -Tag $tag `
175180
-HasUnknownTag $unknownTag -Anchor $anchor -FirstText $rest -FirstColumn $contentColumn
176181
}

src/functions/private/Read-YamlBlockSequence.ps1

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,14 @@ function Read-YamlBlockSequence {
6262
if (-not (Test-YamlIndicator -Text $content -Indicator '-')) {
6363
break
6464
}
65-
$rest = $content.Substring(1).TrimStart()
66-
$itemColumn = $Indent + 1 + ($content.Substring(1).Length - $content.Substring(1).TrimStart().Length)
65+
$rest = $content.Substring(1).TrimStart(' ', "`t")
66+
$itemColumn = $Indent + 1 + (
67+
$content.Substring(1).Length -
68+
$content.Substring(1).TrimStart(' ', "`t").Length
69+
)
6770
}
6871

69-
if ([string]::IsNullOrWhiteSpace((Get-YamlContentWithoutComment -Text $rest))) {
72+
if ([string]::IsNullOrEmpty((Get-YamlContentWithoutComment -Text $rest))) {
7073
$line = $Context.LineIndex
7174
$Context.LineIndex++
7275
Skip-YamlBlockTrivia -Context $Context

src/functions/private/Read-YamlDirectiveBlock.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function Read-YamlDirectiveBlock {
7171
}
7272
$tagHandles[$handle] = $prefix
7373
} elseif ($directive -cnotmatch (
74-
'^%[A-Za-z0-9]+(?:[ \t]+[^#\s][^#]*?)?(?:[ \t]+#.*)?$'
74+
'^%[A-Za-z0-9]+(?:[ \t]+[^# \t][^#]*?)?(?:[ \t]+#.*)?$'
7575
)) {
7676
throw (New-YamlException -Start $mark -End $mark `
7777
-ErrorId 'YamlInvalidDirective' -Message (

0 commit comments

Comments
 (0)