Skip to content

Commit 73e1e32

Browse files
Keep tag limits stable after formatting
Apply tag limits to decoded effective identities while bounding worst-case percent-encoded presentation and cumulative storage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 0474672 commit 73e1e32

7 files changed

Lines changed: 124 additions & 10 deletions

src/functions/private/ConvertFrom-YamlTagUriEscape.ps1

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ function ConvertFrom-YamlTagUriEscape {
1313
[pscustomobject] $Mark,
1414

1515
[Parameter(Mandatory)]
16-
[string] $Token
16+
[string] $Token,
17+
18+
[Parameter(Mandatory)]
19+
[ValidateRange(1, 1048576)]
20+
[int] $MaxLength
1721
)
1822

1923
$builder = [System.Text.StringBuilder]::new()
@@ -25,14 +29,27 @@ function ConvertFrom-YamlTagUriEscape {
2529
if ($character -ne '%') {
2630
if ($escapedBytes.Count -gt 0) {
2731
try {
28-
[void] $builder.Append($utf8.GetString($escapedBytes.ToArray()))
32+
$decoded = $utf8.GetString($escapedBytes.ToArray())
2933
} catch [System.Text.DecoderFallbackException] {
3034
throw (New-YamlException -Start $Mark -End $Mark -ErrorId 'YamlInvalidTag' -Message (
3135
"The tag token '$Token' contains an invalid UTF-8 escape sequence."
3236
))
3337
}
38+
if ($builder.Length + $decoded.Length -gt $MaxLength) {
39+
throw (New-YamlException -Start $Mark -End $Mark `
40+
-ErrorId 'YamlTagLimitExceeded' -Message (
41+
"A YAML tag exceeds the configured limit of $MaxLength characters."
42+
))
43+
}
44+
[void] $builder.Append($decoded)
3445
$escapedBytes.Clear()
3546
}
47+
if ($builder.Length -ge $MaxLength) {
48+
throw (New-YamlException -Start $Mark -End $Mark `
49+
-ErrorId 'YamlTagLimitExceeded' -Message (
50+
"A YAML tag exceeds the configured limit of $MaxLength characters."
51+
))
52+
}
3653
[void] $builder.Append($character)
3754
continue
3855
}
@@ -54,12 +71,19 @@ function ConvertFrom-YamlTagUriEscape {
5471

5572
if ($escapedBytes.Count -gt 0) {
5673
try {
57-
[void] $builder.Append($utf8.GetString($escapedBytes.ToArray()))
74+
$decoded = $utf8.GetString($escapedBytes.ToArray())
5875
} catch [System.Text.DecoderFallbackException] {
5976
throw (New-YamlException -Start $Mark -End $Mark -ErrorId 'YamlInvalidTag' -Message (
6077
"The tag token '$Token' contains an invalid UTF-8 escape sequence."
6178
))
6279
}
80+
if ($builder.Length + $decoded.Length -gt $MaxLength) {
81+
throw (New-YamlException -Start $Mark -End $Mark `
82+
-ErrorId 'YamlTagLimitExceeded' -Message (
83+
"A YAML tag exceeds the configured limit of $MaxLength characters."
84+
))
85+
}
86+
[void] $builder.Append($decoded)
6387
}
6488

6589
$builder.ToString()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
function Get-YamlTagPresentationLimit {
2+
<#
3+
.SYNOPSIS
4+
Gets an allocation-safe presentation bound for a decoded tag limit.
5+
#>
6+
[CmdletBinding()]
7+
[OutputType([int])]
8+
param (
9+
[Parameter(Mandatory)]
10+
[pscustomobject] $Context,
11+
12+
[switch] $Token
13+
)
14+
15+
# One UTF-16 code unit can require three UTF-8 bytes, each written as %XX.
16+
$limit = 9L * $Context.MaxTagLength
17+
if ($Token) {
18+
$maximumHandleLength = 0
19+
foreach ($handle in $Context.TagHandles.Keys) {
20+
$maximumHandleLength = [Math]::Max($maximumHandleLength, $handle.Length)
21+
}
22+
$limit += $maximumHandleLength + 3L
23+
}
24+
25+
[int] [Math]::Min($limit, [int]::MaxValue)
26+
}

src/functions/private/Read-YamlDirectiveBlock.ps1

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ function Read-YamlDirectiveBlock {
5858
"The tag handle '$handle' is declared more than once."
5959
))
6060
}
61-
if ($prefix.Length -gt $Context.MaxTagLength) {
61+
if ($prefix.Length -gt (Get-YamlTagPresentationLimit -Context $Context)) {
6262
throw (New-YamlException -Start $mark -End $mark `
6363
-ErrorId 'YamlTagLimitExceeded' -Message (
64-
"A YAML tag prefix exceeds the configured limit of $($Context.MaxTagLength) characters."
64+
"A YAML tag prefix cannot fit the configured limit of $($Context.MaxTagLength) decoded characters."
6565
))
6666
}
6767
if (-not $prefix.Equals('!', [System.StringComparison]::Ordinal) -and
@@ -71,6 +71,8 @@ function Read-YamlDirectiveBlock {
7171
"The TAG directive prefix '$prefix' contains invalid URI text."
7272
))
7373
}
74+
$null = ConvertFrom-YamlTagUriEscape -Text $prefix -Mark $mark -Token $prefix `
75+
-MaxLength $Context.MaxTagLength
7476
$tagHandles[$handle] = $prefix
7577
} elseif (-not (Test-YamlReservedDirective -Directive $directive)) {
7678
throw (New-YamlException -Start $mark -End $mark `

src/functions/private/Read-YamlFlowNode.ps1

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,11 @@ function Read-YamlFlowNode {
6262
Move-YamlCursor -Cursor $Cursor -Context $Context
6363
}
6464
}
65-
if ($Cursor.Index - $tokenStart -gt $Context.MaxTagLength) {
65+
if ($Cursor.Index - $tokenStart -gt (
66+
Get-YamlTagPresentationLimit -Context $Context -Token
67+
)) {
6668
throw (New-YamlException -Start $start -End $start -ErrorId 'YamlTagLimitExceeded' -Message (
67-
"A YAML tag token exceeds the configured limit of $($Context.MaxTagLength) characters."
69+
"A YAML tag token cannot fit the configured limit of $($Context.MaxTagLength) decoded characters."
6870
))
6971
}
7072
$resolved = Resolve-YamlTag -Token $Context.Text.Substring(

src/functions/private/Read-YamlNodeProperty.ps1

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ function Read-YamlNodeProperty {
5454
$position++
5555
}
5656
}
57-
if ($position - $start -gt $Context.MaxTagLength) {
57+
if ($position - $start -gt (
58+
Get-YamlTagPresentationLimit -Context $Context -Token
59+
)) {
5860
$mark = New-YamlMark -Index ($Context.LineStarts[$Line] + $Column + $start) -Line $Line `
5961
-Column ($Column + $start)
6062
throw (New-YamlException -Start $mark -End $mark -ErrorId 'YamlTagLimitExceeded' -Message (
61-
"A YAML tag token exceeds the configured limit of $($Context.MaxTagLength) characters."
63+
"A YAML tag token cannot fit the configured limit of $($Context.MaxTagLength) decoded characters."
6264
))
6365
}
6466
$token = $Text.Substring($start, $position - $start)

src/functions/private/Resolve-YamlTag.ps1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ function Resolve-YamlTag {
7171
$expanded = if ($isNonSpecific) {
7272
''
7373
} else {
74-
ConvertFrom-YamlTagUriEscape -Text ($prefix + $suffix) -Mark $Mark -Token $Token
74+
ConvertFrom-YamlTagUriEscape -Text ($prefix + $suffix) -Mark $Mark -Token $Token `
75+
-MaxLength $Context.MaxTagLength
7576
}
7677
$expandedLength = $expanded.Length
7778
if ($expandedLength -gt $Context.MaxTagLength) {

tests/Format-Yaml.Tests.ps1

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,63 @@ float: .nan
270270
}
271271

272272
Context 'Validation and parser limits' {
273+
It 'keeps exact effective tag-length boundaries idempotent' -ForEach @(
274+
@{
275+
Name = 'local'
276+
Yaml = '!abc value'
277+
TooLong = '!abcd value'
278+
Limit = 4
279+
}
280+
@{
281+
Name = 'verbatim'
282+
Yaml = '!<a%20b> value'
283+
TooLong = '!<a%20bc> value'
284+
Limit = 3
285+
}
286+
@{
287+
Name = 'expanded handle'
288+
Yaml = "%TAG !e! tag:x%2C`n---`n!e!a value"
289+
TooLong = "%TAG !e! tag:x%2C`n---`n!e!ab value"
290+
Limit = 7
291+
}
292+
) {
293+
$first = $Yaml | Format-Yaml -MaxTagLength $Limit
294+
$second = $first | Format-Yaml -MaxTagLength $Limit
295+
$failure = Get-TestYamlFailure {
296+
$TooLong | Format-Yaml -MaxTagLength $Limit
297+
}
298+
299+
$second | Should -BeExactly $first
300+
$failure.Exception.Data['YamlErrorId'] |
301+
Should -BeExactly 'YamlTagLimitExceeded'
302+
}
303+
304+
It 'keeps the default tag-length boundary idempotent' {
305+
$accepted = '!' + ('a' * 1023) + ' value'
306+
$rejected = '!' + ('a' * 1024) + ' value'
307+
308+
$first = $accepted | Format-Yaml
309+
($first | Format-Yaml) | Should -BeExactly $first
310+
$failure = Get-TestYamlFailure {
311+
$rejected | Format-Yaml
312+
}
313+
$failure.Exception.Data['YamlErrorId'] |
314+
Should -BeExactly 'YamlTagLimitExceeded'
315+
}
316+
317+
It 'keeps cumulative effective tag budgets idempotent' {
318+
$yaml = "!abc one`n---`n!def two"
319+
$first = $yaml | Format-Yaml -MaxTagLength 4 -MaxTotalTagLength 8
320+
321+
($first | Format-Yaml -MaxTagLength 4 -MaxTotalTagLength 8) |
322+
Should -BeExactly $first
323+
$failure = Get-TestYamlFailure {
324+
$yaml | Format-Yaml -MaxTagLength 4 -MaxTotalTagLength 7
325+
}
326+
$failure.Exception.Data['YamlErrorId'] |
327+
Should -BeExactly 'YamlTagLimitExceeded'
328+
}
329+
273330
It 'classifies every parser resource limit like ConvertFrom-Yaml' -ForEach @(
274331
@{ Name = 'depth'; Yaml = "a:`n b:`n c: value"; Parameters = @{ Depth = 2 } }
275332
@{ Name = 'nodes'; Yaml = '[one, two]'; Parameters = @{ MaxNodes = 2 } }

0 commit comments

Comments
 (0)