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.
This commit is contained in:
Adam Gauthier 2019-02-20 00:37:37 -05:00 committed by Ilya
parent 36117a4d55
commit caf8ac6a6a
4 changed files with 201 additions and 15 deletions

View file

@ -36,6 +36,13 @@ namespace Microsoft.PowerShell.Commands
[Parameter()]
public SwitchParameter AsHashtable { get; set; }
/// <summary>
/// Gets or sets the maximum depth the JSON input is allowed to have. By default, it is 1024.
/// </summary>
[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)
{

View file

@ -19,7 +19,7 @@ namespace Microsoft.PowerShell.Commands
/// <summary>
/// JsonObject class.
/// </summary>
[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
/// <param name="input">The json text to convert.</param>
/// <param name="error">An error record if the conversion failed.</param>
/// <returns>A PSObject.</returns>
[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
/// <param name="error">An error record if the conversion failed.</param>
/// <returns>A <see cref="System.Management.Automation.PSObject"/> or a <see cref="System.Collections.Hashtable"/>
/// if the <paramref name="returnHashtable"/> parameter is true.</returns>
[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);
}
/// <summary>
/// Convert a JSON string back to an object of type <see cref="System.Management.Automation.PSObject"/> or
/// <see cref="System.Collections.Hashtable"/> depending on parameter <paramref name="returnHashtable"/>.
/// </summary>
/// <param name="input">The JSON text to convert.</param>
/// <param name="returnHashtable">True if the result should be returned as a <see cref="System.Collections.Hashtable"/>
/// instead of a <see cref="System.Management.Automation.PSObject"/>.</param>
/// <param name="maxDepth">The max depth allowed when deserializing the json input. Set to null for no maximum.</param>
/// <param name="error">An error record if the conversion failed.</param>
/// <returns>A <see cref="System.Management.Automation.PSObject"/> or a <see cref="System.Collections.Hashtable"/>
/// if the <paramref name="returnHashtable"/> parameter is true.</returns>
[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)

View file

@ -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 <AsHashtable>' -TestCase $testCasesWithAndWithoutAsHashtableSwitch {
It 'Can convert a single-line object with AsHashtable switch set to <AsHashtable>' -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 <AsHashtable>' -TestCase $testCasesWithAndWithoutAsHashtableSwitch {
It 'Can convert one string-per-object with AsHashtable switch set to <AsHashtable>' -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 <AsHashtable>' -TestCase $testCasesWithAndWithoutAsHashtableSwitch {
It 'Can convert multi-line object with AsHashtable switch set to <AsHashtable>' -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 <AsHashtable>' -TestCase $testCasesWithAndWithoutAsHashtableSwitch {
It 'Can convert an object with Newtonsoft.Json metadata properties with AsHashtable switch set to <AsHashtable>' -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 <AsHashtable>' -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 <AsHashtable>' -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 <Depth> and AsHashtable switch set to <AsHashtable>' -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 <Depth> and AsHashtable switch set to <AsHashtable>' -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 <Depth> and AsHashtable switch set to <AsHashtable>' -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"
}
}

View file

@ -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 ''<name>'' with -ReturnHashTable:$<ReturnHashTable>' -TestCase @(
It 'No error for valid string ''<name>'' with -ReturnHashTable:<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 ''<name>'' with -ReturnHashTable:$<ReturnHashTable>' -TestCase @(
It 'Throw ArgumentException for invalid string ''<name>'' with -ReturnHashTable:<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:<Depth> with -ReturnHashTable:<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:<Depth> with -ReturnHashTable:<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:<Depth> with -ReturnHashTable:<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)