From caf8ac6a6af4a370a5762046d3ed630f5775c55e Mon Sep 17 00:00:00 2001 From: Adam Gauthier Date: Wed, 20 Feb 2019 00:37:37 -0500 Subject: [PATCH] Add configurable maximum depth in ConvertFrom-Json with `-Depth` (#8199) Adds an optional -Depth parameter to the cmdlet which lets the user to specify a maximum depth allowed for deserialization, which will overwrite the default maximum of 1024. --- .../WebCmdlet/ConvertFromJsonCommand.cs | 9 +- .../commands/utility/WebCmdlet/JsonObject.cs | 28 +++- .../ConvertFrom-Json.Tests.ps1 | 122 +++++++++++++++++- .../JsonObject.Tests.ps1 | 57 +++++++- 4 files changed, 201 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertFromJsonCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertFromJsonCommand.cs index 95ef144b4..3556c1ac3 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertFromJsonCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertFromJsonCommand.cs @@ -36,6 +36,13 @@ namespace Microsoft.PowerShell.Commands [Parameter()] public SwitchParameter AsHashtable { get; set; } + /// + /// Gets or sets the maximum depth the JSON input is allowed to have. By default, it is 1024. + /// + [Parameter()] + [ValidateRange(ValidateRangeKind.Positive)] + public int Depth { get; set; } = 1024; + #endregion parameters #region overrides @@ -100,7 +107,7 @@ namespace Microsoft.PowerShell.Commands private bool ConvertFromJsonHelper(string input) { ErrorRecord error = null; - object result = JsonObject.ConvertFromJson(input, AsHashtable.IsPresent, out error); + object result = JsonObject.ConvertFromJson(input, AsHashtable.IsPresent, Depth, out error); if (error != null) { diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs index 21cd6d6ac..0c80e9808 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs @@ -19,7 +19,7 @@ namespace Microsoft.PowerShell.Commands /// /// JsonObject class. /// - [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Preferring Json over JSON")] public static class JsonObject { #region HelperTypes @@ -121,7 +121,7 @@ namespace Microsoft.PowerShell.Commands /// The json text to convert. /// An error record if the conversion failed. /// A PSObject. - [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Preferring Json over JSON")] public static object ConvertFromJson(string input, out ErrorRecord error) { return ConvertFromJson(input, returnHashtable: false, out error); @@ -137,8 +137,25 @@ namespace Microsoft.PowerShell.Commands /// An error record if the conversion failed. /// A or a /// if the parameter is true. - [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Preferring Json over JSON")] public static object ConvertFromJson(string input, bool returnHashtable, out ErrorRecord error) + { + return ConvertFromJson(input, returnHashtable, maxDepth: 1024, out error); + } + + /// + /// Convert a JSON string back to an object of type or + /// depending on parameter . + /// + /// The JSON text to convert. + /// True if the result should be returned as a + /// instead of a . + /// The max depth allowed when deserializing the json input. Set to null for no maximum. + /// An error record if the conversion failed. + /// A or a + /// if the parameter is true. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Preferring Json over JSON")] + public static object ConvertFromJson(string input, bool returnHashtable, int? maxDepth, out ErrorRecord error) { if (input == null) { @@ -171,7 +188,7 @@ namespace Microsoft.PowerShell.Commands // This TypeNameHandling setting is required to be secure. TypeNameHandling = TypeNameHandling.None, MetadataPropertyHandling = MetadataPropertyHandling.Ignore, - MaxDepth = 1024 + MaxDepth = maxDepth }); switch (obj) @@ -185,7 +202,8 @@ namespace Microsoft.PowerShell.Commands return returnHashtable ? PopulateHashTableFromJArray(list, out error) : PopulateFromJArray(list, out error); - default: return obj; + default: + return obj; } } catch (JsonException je) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1 index e4a32a5ee..8160864dd 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1 @@ -1,6 +1,36 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -Describe 'ConvertFrom-Json' -tags "CI" { + +function New-NestedJson { + Param( + [ValidateRange(1, 2048)] + [int] + $Depth + ) + + $nestedJson = "true" + + $Depth..1 | ForEach-Object { + $nestedJson = '{"' + $_ + '":' + $nestedJson + '}' + } + + return $nestedJson +} + +function Count-ObjectDepth { + Param([PSCustomObject] $InputObject) + + for ($i=1; $i -le 2048; $i++) + { + $InputObject = Select-Object -InputObject $InputObject -ExpandProperty $i + if ($InputObject -eq $true) + { + return $i + } + } +} + +Describe 'ConvertFrom-Json Unit Tests' -tags "CI" { BeforeAll { $testCasesWithAndWithoutAsHashtableSwitch = @( @@ -9,12 +39,12 @@ Describe 'ConvertFrom-Json' -tags "CI" { ) } - It 'Can convert a single-line object with AsHashtable switch set to ' -TestCase $testCasesWithAndWithoutAsHashtableSwitch { + It 'Can convert a single-line object with AsHashtable switch set to ' -TestCases $testCasesWithAndWithoutAsHashtableSwitch { Param($AsHashtable) ('{"a" : "1"}' | ConvertFrom-Json -AsHashtable:$AsHashtable).a | Should -Be 1 } - It 'Can convert one string-per-object with AsHashtable switch set to ' -TestCase $testCasesWithAndWithoutAsHashtableSwitch { + It 'Can convert one string-per-object with AsHashtable switch set to ' -TestCases $testCasesWithAndWithoutAsHashtableSwitch { Param($AsHashtable) $json = @('{"a" : "1"}', '{"a" : "x"}') | ConvertFrom-Json -AsHashtable:$AsHashtable $json.Count | Should -Be 2 @@ -25,7 +55,7 @@ Describe 'ConvertFrom-Json' -tags "CI" { } } - It 'Can convert multi-line object with AsHashtable switch set to ' -TestCase $testCasesWithAndWithoutAsHashtableSwitch { + It 'Can convert multi-line object with AsHashtable switch set to ' -TestCases $testCasesWithAndWithoutAsHashtableSwitch { Param($AsHashtable) $json = @('{"a" :', '"x"}') | ConvertFrom-Json -AsHashtable:$AsHashtable $json.a | Should -Be 'x' @@ -35,7 +65,7 @@ Describe 'ConvertFrom-Json' -tags "CI" { } } - It 'Can convert an object with Newtonsoft.Json metadata properties with AsHashtable switch set to ' -TestCase $testCasesWithAndWithoutAsHashtableSwitch { + It 'Can convert an object with Newtonsoft.Json metadata properties with AsHashtable switch set to ' -TestCases $testCasesWithAndWithoutAsHashtableSwitch { Param($AsHashtable) $id = 13 $type = 'Calendar.Months.December' @@ -52,4 +82,86 @@ Describe 'ConvertFrom-Json' -tags "CI" { $json | Should -BeOfType Hashtable } } + + It 'Can convert an object of depth 1024 by default with AsHashtable switch set to ' -TestCases $testCasesWithAndWithoutAsHashtableSwitch { + Param($AsHashtable) + $nestedJson = New-NestedJson -Depth 1024 + + $json = $nestedJson | ConvertFrom-Json -AsHashtable:$AsHashtable + + if ($AsHashtable) + { + $json | Should -BeOfType Hashtable + } + else + { + $json | Should -BeOfType PSCustomObject + } + } + + It 'Fails to convert an object of depth higher than 1024 by default with AsHashtable switch set to ' -TestCases $testCasesWithAndWithoutAsHashtableSwitch { + Param($AsHashtable) + $nestedJson = New-NestedJson -Depth 1025 + + { $nestedJson | ConvertFrom-Json -AsHashtable:$AsHashtable } | + Should -Throw -ErrorId "System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand" + } +} + +Describe 'ConvertFrom-Json -Depth Tests' -tags "Feature" { + + BeforeAll { + $testCasesJsonDepthWithAndWithoutAsHashtableSwitch = @( + @{ Depth = 2; AsHashtable = $true } + @{ Depth = 2; AsHashtable = $false } + @{ Depth = 200; AsHashtable = $true } + @{ Depth = 200; AsHashtable = $false } + @{ Depth = 2000; AsHashtable = $true } + @{ Depth = 2000; AsHashtable = $false } + ) + } + + It 'Can convert an object with depth less than Depth param set to and AsHashtable switch set to ' -TestCases $testCasesJsonDepthWithAndWithoutAsHashtableSwitch { + Param($AsHashtable, $Depth) + $nestedJson = New-NestedJson -Depth ($Depth - 1) + + $json = $nestedJson | ConvertFrom-Json -AsHashtable:$AsHashtable -Depth $Depth + + if ($AsHashtable) + { + $json | Should -BeOfType Hashtable + } + else + { + $json | Should -BeOfType PSCustomObject + } + + (Count-ObjectDepth -InputObject $json) | Should -Be ($Depth - 1) + } + + It 'Can convert an object with depth equal to Depth param set to and AsHashtable switch set to ' -TestCases $testCasesJsonDepthWithAndWithoutAsHashtableSwitch { + Param($AsHashtable, $Depth) + $nestedJson = New-NestedJson -Depth:$Depth + + $json = $nestedJson | ConvertFrom-Json -AsHashtable:$AsHashtable -Depth $Depth + + if ($AsHashtable) + { + $json | Should -BeOfType Hashtable + } + else + { + $json | Should -BeOfType PSCustomObject + } + + (Count-ObjectDepth -InputObject $json) | Should -Be $Depth + } + + It 'Fails to convert an object with greater depth than Depth param set to and AsHashtable switch set to ' -TestCases $testCasesJsonDepthWithAndWithoutAsHashtableSwitch { + Param($AsHashtable, $Depth) + $nestedJson = New-NestedJson -Depth ($Depth + 1) + + { $nestedJson | ConvertFrom-Json -AsHashtable:$AsHashtable -Depth $Depth } | + Should -Throw -ErrorId "System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand" + } } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/JsonObject.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/JsonObject.Tests.ps1 index a23d404bb..2c61abd29 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/JsonObject.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/JsonObject.Tests.ps1 @@ -5,9 +5,34 @@ Describe 'Unit tests for JsonObject' -tags "CI" { BeforeAll { $jsonWithEmptyKey = '{"": "Value"}' $jsonContainingKeysWithDifferentCasing = '{"key1": "Value1", "Key1": "Value2"}' + + $testCasesJsonDepthWithAndWithoutReturnHashTable = @( + @{ Depth = 2; ReturnHashTable = $true } + @{ Depth = 2; ReturnHashTable = $false } + @{ Depth = 200; ReturnHashTable = $true } + @{ Depth = 200; ReturnHashTable = $false } + @{ Depth = 2000; ReturnHashTable = $true } + @{ Depth = 2000; ReturnHashTable = $false } + ) + + function New-NestedJson { + Param( + [ValidateRange(1, 2048)] + [int] + $Depth + ) + + $nestedJson = "true" + + $Depth..1 | ForEach-Object { + $nestedJson = '{"' + $_ + '":' + $nestedJson + '}' + } + + return $nestedJson + } } - It 'No error for valid string '''' with -ReturnHashTable:$' -TestCase @( + It 'No error for valid string '''' with -ReturnHashTable:' -TestCases @( @{ name = "null"; str = $null; ReturnHashTable = $true } @{ name = "empty"; str = ""; ReturnHashTable = $true } @{ name = "spaces"; str = " "; ReturnHashTable = $true } @@ -23,7 +48,7 @@ Describe 'Unit tests for JsonObject' -tags "CI" { $errRecord | Should -BeNullOrEmpty } - It 'Throw ArgumentException for invalid string '''' with -ReturnHashTable:$' -TestCase @( + It 'Throw ArgumentException for invalid string '''' with -ReturnHashTable:' -TestCases @( @{ name = "plain text"; str = "plaintext"; ReturnHashTable = $true } @{ name = "part"; str = '{"a" :'; ReturnHashTable = $true } @{ name = "plain text"; str = "plaintext"; ReturnHashTable = $false } @@ -34,7 +59,31 @@ Describe 'Unit tests for JsonObject' -tags "CI" { { [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($str, $ReturnHashTable, [ref]$errRecord) } | Should -Throw -ErrorId "ArgumentException" } - Context 'Empty key name' { + It 'Can Convert json with depth less than -Depth: with -ReturnHashTable:' -TestCases $testCasesJsonDepthWithAndWithoutReturnHashTable { + param ($Depth, $ReturnHashTable) + $errRecord = $null + $json = New-NestedJson -Depth ($Depth - 1) + [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($json, $ReturnHashTable, $Depth, [ref]$errRecord) + $errRecord | Should -BeNullOrEmpty + } + + It 'Can Convert json with depth equal to -Depth: with -ReturnHashTable:' -TestCases $testCasesJsonDepthWithAndWithoutReturnHashTable { + param ($Depth, $ReturnHashTable) + $errRecord = $null + $json = New-NestedJson -Depth $Depth + [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($json, $ReturnHashTable, $Depth, [ref]$errRecord) + $errRecord | Should -BeNullOrEmpty + } + + It 'Throws ArgumentException for json with greater depth than -Depth: with -ReturnHashTable:' -TestCases $testCasesJsonDepthWithAndWithoutReturnHashTable { + param ($Depth, $ReturnHashTable) + $errRecord = $null + $json = New-NestedJson -Depth ($Depth + 1) + { [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($json, $ReturnHashTable, $Depth, [ref]$errRecord) } | + Should -Throw -ErrorId "ArgumentException" + } + + Context 'Empty key name' { It 'Throw InvalidOperationException when json contains empty key name' { $errorRecord = $null [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($jsonWithEmptyKey, [ref]$errorRecord) @@ -51,7 +100,7 @@ Describe 'Unit tests for JsonObject' -tags "CI" { } Context 'Keys with different casing ' { - + It 'Throw InvalidOperationException when json contains key with different casing' { $errorRecord = $null [Microsoft.PowerShell.Commands.JsonObject]::ConvertFromJson($jsonContainingKeysWithDifferentCasing, [ref]$errorRecord)