-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Add configurable maximum depth in ConvertFrom-Json with -Depth
#8199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add configurable maximum depth in ConvertFrom-Json with -Depth
#8199
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need PowerShell Committee approvment:
- Add new parameter
-MaxDepth, should it beintorint?? - Add new overload
public static object ConvertFromJson(string input, bool returnHashtable, int? maxDepth, out ErrorRecord error)/cc @SteveL-MSFT
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be:
| public int? MaxDepth { get; set; } | |
| public int MaxDepth { get; set; } = int.MaxValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think we have any default cmdlets with nullable input types, so I don't see any need for that. I would think that since it's a MaxDepth parameter we could either:
- Opt for
uint(because negative depth is insensible) with a maximum set even perhaps at something comparable to theConvertTo-Jsoncommand which has2as its default-MaxDepth, or - Simply set the default to
-1and have the code paths treat that as "no maximum depth".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the feedback, as this is my first feature contribution, I wasn't sure how to implement everything 😅
@iSazonov your suggestion would mean there would be no way to differentiate no maximum depth and the user wanting a maximum depth of 2147483647. I think the value representing no maximum depth should really be outside of the user input range for maximum depth (1 - 2147483647)!
@vexx32 I could do a uint with range of 1 - int.MaxValue and use 0 as default value representing no maximum. I could also do int with -1 as default value representing no maximum. If that's what the team prefers, I don't mind, I was just using null as a way to represent the parameter not being specified. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you have a point there; a maximum depth of 0 is also pretty silly. However, given that the underlying json API uses int for these parameters, as @iSazonov pointed out, we can't safely use uint as it would allow the user to specify values that the underlying API can't handle.
I think we need some clarification on whether the json API itself has a defined specification for "no maximum depth" and what that would look like.
@markekraus I know you worked on Invoke-RestMethod a good bit, do you have any insight to offer on the json API that might help here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vexx32 I meant I could do uint with [ValidateRange(1, int.MaxValue)], effectively not allowing user input values outside the underlying API. (but we'll have to cast the value so it might just be better to use int directly?)
Also, I was proposing 0 because the underlying API doesn't allow 0 for MaxDepth, only 1 - int.MaxValue (it throws an exception at runtime, see here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, that's just funny at that point. If you're going to throw that early... why not just make it uint haha!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The underlying type is an int, so we should probably accept an int with the [ValidateRange(1, int.MaxValue)]. realistically, an JSON with a depth greater than int32.MaxValue is probably bad JSON to begin with.... A nullable type for this would be weird. accepting -1 is also weird.
|
@PowerShell/powershell-committee reviewed this, the design should be that there is a |
|
@SteveL-MSFT with all due respect, may I ask to reconsider this decision? The
PS C:\> ConvertTo-Json (ConvertFrom-Json '{"1":{"2":{"3":{"4":{"5":1}}}}}')
{
"1": {
"2": {
"3": "@{4=}"
}
}
}
Furthermore, I believe the current I also think putting I know it would be a breaking change to have no maximum be the new default, but is it really a common scenario that someone is relying on the default behavior failing with input of depth higher than 1024? I think handling higher depth with this new parameter is the perfect opportunity to change the default behavior to something more intuitive: the cmdlets should handle whatever is thrown at them, if the user expects the input to cause a DoS in their system (which I assume was the reason for this default max depth), then they should specify a maximum depth optionally. |
|
So we should discuss:
My thoughts:
/cc @markekraus @mklement0 What do you think? |
|
@louistio to be clear, the decision regarding It would also be useful if you can provide examples where the current limit of 1024 is insufficient. |
|
@SteveL-MSFT Thank you! To be clear the naming is not a big concern to me, it's more about the bad user experience of having a default that I described later in my post. 😄 |
|
@PowerShell/powershell-committee had a very bikeshed conversation about this, and we understand the parallel concepts argument with this rough visualization: Does anyone else actually care? |
|
Two cents:
|
|
@markekraus @mklement0 Have you any thoughts about consistency of Depth parameters? |
|
I agree with @rjmholt |
|
@joeyaiello @SteveL-MSFT Seems we can make final conclusion and continue the PR. |
|
In terms of naming, Regrettably, exceeding that maximum depth doesn't cause an error in In a utopian world, unencumbered by backward-compatibility concerns:
|
|
@rjmholt Fair point about powershell being secure by default. @mklement0 I agree with everything you're saying, I think you basically put into words what I was struggling to convey. I like the idea of another switch for no I would like to implement an equivalent to |
|
@louistio What would be the difference between |
|
@markekraus As mentioned above, the behavior difference resides in the cmdlet failing on input exceeding the maximum depth. Currently What I'm saying is I'd be fine with changing |
|
I think it would be confusing to have both Depth and Max Depth. We should have one for the depth and one to adjust the behavior of that depth. |
|
True, having both
(What is conceptually)
I personally then don't see a need for any other parameters - I think opting into levels beyond the fixed limit with an explicit number or even with In the absence of using
As for required changes and backward compatibility:
|
|
There are 2 things: 1) The maximum depth you want to convert and 2) the behavior of that conversion. They both need to be configurable, IMO and just relying on a single param for both would be a pain. There are a very large number of objects passed to Consider the Current behavior:
I would ultimately like to see it as this:
Ideally we would would want a But, keeping in mind that we could add parameters to control the behavior later, for this PR all we need is to add A bit of flavor text: A JSON object over 1024 in depth is an outlier. Other than abused/broken APIs or contrived examples, I have rarely seen a legit JSON objet that is deeper than ~30. On the flip-side, infinitely deep objects are very commonly thrown at ConvertTo-Json. (Based on my experience, YMMV.) |
I think it is better to make both changes for Depth in the PR to get clean history and continue with rest ideas in follow PRs. |
|
Thanks for the analysis, @markekraus, but I still think
To put it differently: the hard-coded max. depth is then no longer a default value for If you have a "runaway" tree of infinite depth, then the hard-coded max. will save you. |
|
@PowerShell/powershell-committee appreciates the discussion and we still suggest that we have the single |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebased and changed according to comments from the committee. I took the liberty of having the option to set -Depth to 0 to mean no maximum depth, as I think it's a good option to provide users and the committee did not say anything against it.
| [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")] | ||
| public static object ConvertFromJson(string input, bool returnHashtable, out ErrorRecord error) | ||
| { | ||
| return ConvertFromJson(input, returnHashtable, maxDepth: 1024, out error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering if a custom type SerializerSettings to encapsulate default 1024 maxDepth and false returnHashtable here would be better than adding overloads.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if there are other settings we would want in the future, then that would be a better approach
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would consider the option to set -Depth less than 0 for no maximum depth.
0 or $Null are often the result of a wrong calculation. Besides, 0 could technically still be a desired "depth".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Besides,
0could technically still be a desired "depth".
How so? Wouldn't that mean you want the deserialization to always fail? Is that really a use case? Also, the underlying Newtonsoft.Json does not allow 0 or less as MaxDepth, it throws at runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a sample recurring hash table like:
$ht = @{
Name = "Tree"
Parent = @{
Name = "Parent"
Child = @{
Name = "Child"
}
}
}
$ht.Parent.Child.Parent = $ht.ParentA $ht | ConvertTo-Json -Depth 1 results in:
{
"Parent": {
"Child": "System.Collections.Hashtable",
"Name": "Parent"
},
"Name": "Tree"
}
I was expecting for $ht | ConvertTo-Json -Depth 0 to result in:
{
"Parent": "System.Collections.Hashtable",
"Name": "Tree"
}
But I indeed get an error:
ConvertTo-Json : Cannot validate argument on parameter 'Depth'. The 0 argument is less than the minimum allowed range
of 1. Supply an argument that is greater than or equal to 1 and then try the command again.
At line:1 char:29
- $ht | ConvertTo-Json -Depth 0
~
- CategoryInfo : InvalidData: (:) [ConvertTo-Json], ParameterBindingValidationException
- FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.PowerShell.Commands.ConvertToJsonCommand
Anyways, wouldn't it be better to get either a completely flat output or an error in case of a (incorrect calculated/unassigned/mistyped variable) Depth of 0 or $Null?
I think a negative Depth would just be more knowingly chosen.
In other words, I do not really expect it to be a desired used case, but in the case of a scripting failure, I think a flat result or an error is more clear then a possible performance issue.
Just a thought, the decision is up to you...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still of the opinion that unlimited is not needed. 1 to int32.MaxValue is sufficient. Any JSON over Int32.MaxValue is either broken or contrived.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the real world, I expect the default of 1024 to be sufficient, so I would be ok with not needing unlimited. -Depth 0 for only the top level makes sense and consistent with Get-ChildItem -Depth 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@iRon7 after thinking it through, I agree a negative value might be better than 0.
@markekraus I understand powershell needs to be secure by default but I think this is an opportunity to provide the option for people to just say "I know what I'm doing, get out of my way". Sure, you could have them type -Depth:([int]::MaxValue), but feels less convenient in my opinion and it would still have the overhead (however small) of the serializer repeatedly checking to make sure max depth isn't exceeded (not the case if you pass null).
What do you guys think of having -Depth -1 for unlimited and maybe throw an error on BeginProcessing() if -Depth is 0?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not about security.. it's about absurdity. The only JSON objects that are that deep are contrived (made to be deep for fun or to specifically test depth limits) or broken (whatever produced the JSON had some issue). 1024 is already absurdly deep for a JSON object. We all ready catch outliers with the default setting. Between 1024 and Int32.MaxValue is a the realm of intrigue only. We don't need to have special case logic for that. We don't even really need infinite depth. What is the target audience for such a feature? Who really has objects that deep?
Just because we can, doesn't mean we should I still stand by my statement that 1 to int32.MaxValue is sufficient.
-Depth
|
@markekraus @mklement0 @SteveL-MSFT Please update your review. @louistio Please fix StyleCop issues. |
| MetadataPropertyHandling = MetadataPropertyHandling.Ignore | ||
| }; | ||
|
|
||
| if (maxDepth != null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.MaxDepth == null means no limit, so it doesn't seem this check is necessary
| @{ Depth = 2; AsHashtable = $false } | ||
| @{ Depth = 200; AsHashtable = $true } | ||
| @{ Depth = 200; AsHashtable = $false } | ||
| @{ Depth = 2000; AsHashtable = $true } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How long does this take to run? If longer than 2 secs, we should have them as Feature tests instead of CI.
|
|
||
| It 'Fails to convert an object of depth higher than 1024 by default with AsHashtable switch set to <AsHashtable>' -TestCases $testCasesWithAndWithoutAsHashtableSwitch { | ||
| Param($AsHashtable) | ||
| $nestedJson = GenerateNestedJson -Depth:1989 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't 1025 be sufficient? no need to spend extra cpu here
| @{ Depth = 2000; AsHashtable = $false } | ||
| ) | ||
|
|
||
| function GenerateNestedJson { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prefer New-NestedJson
test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1
Outdated
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1
Outdated
Show resolved
Hide resolved
| function Count-ObjectDepth { | ||
| Param([PSCustomObject] $InputObject) | ||
|
|
||
| for ($i=1; $i -le 100000; $i++) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you have a max depth for checking here, you should probably enforce this limit on new-nestedjson. Also, I'm not sure we need to test this deep, perhaps 2048 is sufficient
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@louistio Have you time to continue?
test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1
Outdated
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1
Outdated
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1
Outdated
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1
Outdated
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1
Outdated
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Utility/JsonObject.Tests.ps1
Outdated
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Utility/JsonObject.Tests.ps1
Outdated
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Utility/JsonObject.Tests.ps1
Outdated
Show resolved
Hide resolved
Co-Authored-By: louistio <adamgauthier12@gmail.com>
|
@iSazonov Hey! Sorry, I wasn't aware of the colon convention 😅 I applied your suggestions through GitHub's integrated tool, but there seems to be error when running the tests without the colons! I get errors like: I'm not super familiar as to why removing colons would cause this, could you point me into the right direction? Thanks! |
|
@louistio Sorry, colons is needed for asHashtable but not for Depth. |
|
@SteveL-MSFT @markekraus Please update your review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor cleanup requested.
src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertFromJsonCommand.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertFromJsonCommand.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs
Outdated
Show resolved
Hide resolved
Co-Authored-By: louistio <adamgauthier12@gmail.com>
|
@SteveL-MSFT Please update your review. |
|
@louistio Please update the PR description and we'll merge. |
|
@iSazonov description updated, I believe this still requires documentation changes, not sure if that prevents from merging. |
|
@louistio Please open new Issue in PowerShell-Docs repo and add a reference to the PR description. |
|
@iSazonov done! |
|
@louistio Thanks for great work! |
PR Summary
The
ConvertFrom-Jsoncmdlet is currently limited to deserializing json objects with a maximum depth of 1024. An exception is thrown if the input's depth reaches that maximum.This PR:
-Depthparameter to the cmdlet which lets the user to specify a maximum depth allowed for deserialization, which will overwrite the default maximum of1024.Closes #3182
PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright headerWIP:to the beginning of the title and remove the prefix when the PR is ready.[feature]if the change is significant or affects feature tests