Skip to content

Commit

Permalink
S5773: VB.NET implementation (#7623)
Browse files Browse the repository at this point in the history
Co-authored-by: mary-georgiou-sonarsource <mary.georgiou@sonarsource.com>
Co-authored-by: Tim Pohlmann <tim.pohlmann@sonarsource.com>
  • Loading branch information
3 people committed Jul 24, 2023
1 parent be9ca30 commit 14115ea
Show file tree
Hide file tree
Showing 15 changed files with 828 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"issues": [
{
"id": "S5773",
"message": "Restrict types of objects allowed to be deserialized.",
"location": {
"uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/Ember-MM/Addons/scraper.EmberCore/TVScraper/clsScrapeTVDB.vb#L150",
"region": {
"startLine": 150,
"startColumn": 41,
"endLine": 150,
"endColumn": 59
}
}
}
]
}
4 changes: 2 additions & 2 deletions analyzers/rspec/cs/S5773.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"title": "Types allowed to be deserialized should be restricted",
"type": "VULNERABILITY",
"status": "ready",
"quickfix": "targeted",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "30min"
Expand All @@ -28,6 +29,5 @@
"5.5.1",
"5.5.3"
]
},
"quickfix": "unknown"
}
}
106 changes: 106 additions & 0 deletions analyzers/rspec/vbnet/S5773.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<h2>Why is this an issue?</h2>
<p>During the deserialization process, the state of an object will be reconstructed from the serialized data stream which can contain dangerous
operations.</p>
<p>For example, a well-known attack vector consists in serializing an object of type <code><a
href="https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.tempfilecollection.-ctor?view=netframework-4.8#System_CodeDom_Compiler_TempFileCollection__ctor">TempFileCollection</a></code>
with arbitrary files (defined by an attacker) which will be deleted on the application deserializing this object (when the <a
href="https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.tempfilecollection.finalize?view=netframework-4.8">finalize() </a>method of
the TempFileCollection object is called). This kind of types are called "<a href="https://github.com/pwntester/ysoserial.net">gadgets</a>".</p>
<p>Instead of using <code>BinaryFormatter</code> and similar serializers, it is recommended to use safer alternatives in most of the cases, such as <a
href="https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.xmlserializer?view=net-5.0">XmlSerializer</a> or <a
href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractserializer?view=net-5.0">DataContractSerializer</a>. If
it’s not possible then try to mitigate the risk by restricting the types allowed to be deserialized:</p>
<ul>
<li> by implementing an "allow-list" of types, but keep in mind that novel dangerous types are regularly discovered and this protection could be
insufficient over time. </li>
<li> or/and implementing a tamper protection, such as <a href="https://en.wikipedia.org/wiki/HMAC">message authentication codes</a> (MAC). This way
only objects serialized with the correct MAC hash will be deserialized. </li>
</ul>
<h3>Noncompliant code example</h3>
<p>For <a
href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.formatters.binary.binaryformatter?view=netframework-4.8">BinaryFormatter</a>,
<a
href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.netdatacontractserializer?view=netframework-4.8">NetDataContractSerializer</a>,
<a
href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.formatters.soap.soapformatter?view=netframework-4.8">SoapFormatter</a>
serializers:</p>
<pre>
Dim myBinaryFormatter = New BinaryFormatter()
myBinaryFormatter.Deserialize(stream) ' Noncompliant: a binder is not used to limit types during deserialization
</pre>
<p><a
href="https://docs.microsoft.com/en-us/dotnet/api/system.web.script.serialization.javascriptserializer?view=netframework-4.8">JavaScriptSerializer</a>
should not use SimpleTypeResolver or other weak resolvers:</p>
<pre>
Dim serializer1 As JavaScriptSerializer = New JavaScriptSerializer(New SimpleTypeResolver()) ' Noncompliant: SimpleTypeResolver is unsecure (every types is resolved)
serializer1.Deserialize(Of ExpectedType)(json)
</pre>
<p><a href="https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.losformatter?view=netframework-4.8">LosFormatter</a> should not be used without
MAC verification:</p>
<pre>
Dim formatter As LosFormatter = New LosFormatter() ' Noncompliant
formatter.Deserialize(fs)
</pre>
<h3>Compliant solution</h3>
<p><a
href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.formatters.binary.binaryformatter?view=netframework-4.8">BinaryFormatter</a>,
<a
href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.netdatacontractserializer?view=netframework-4.8">NetDataContractSerializer
</a>, <a
href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.formatters.soap.soapformatter?view=netframework-4.8">SoapFormatter</a>
serializers should use a binder implementing a whitelist approach to limit types during deserialization (at least one exception should be thrown or a
null value returned):</p>
<pre>
NotInheritable Class CustomBinder
Inherits SerializationBinder
Public Overrides Function BindToType(assemblyName As String, typeName As String) As Type
If Not (Equals(typeName, "type1") OrElse Equals(typeName, "type2") OrElse Equals(typeName, "type3")) Then
Throw New SerializationException("Only type1, type2 and type3 are allowed") ' Compliant
End If
Return Assembly.Load(assemblyName).[GetType](typeName)
End Function
End Class

Dim myBinaryFormatter = New BinaryFormatter()
myBinaryFormatter.Binder = New CustomBinder()
myBinaryFormatter.Deserialize(stream)
</pre>
<p><a
href="https://docs.microsoft.com/en-us/dotnet/api/system.web.script.serialization.javascriptserializer?view=netframework-4.8">JavaScriptSerializer</a>
should use a resolver implementing a whitelist to limit types during deserialization (at least one exception should be thrown or a null value
returned):</p>
<pre>
Public Class CustomSafeTypeResolver
Inherits JavaScriptTypeResolver
Public Overrides Function ResolveType(id As String) As Type
If Not Equals(id, "ExpectedType") Then
Throw New ArgumentNullException("Only ExpectedType is allowed during deserialization") ' Compliant
End If
Return Type.[GetType](id)
End Function
End Class

Dim serializer As JavaScriptSerializer = New JavaScriptSerializer(New CustomSafeTypeResolver()) ' Compliant
serializer.Deserialize(Of ExpectedType)(json)
</pre>
<p><a href="https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.losformatter?view=netframework-4.8">LosFormatter</a> serializer with MAC
verification:</p>
<pre>
Dim formatter As LosFormatter = New LosFormatter(True, secret) ' Compliant
formatter.Deserialize(fs)
</pre>
<h2>Resources</h2>
<ul>
<li> <a href="https://owasp.org/Top10/A08_2021-Software_and_Data_Integrity_Failures/">OWASP Top 10 2021 Category A8</a> - Software and Data
Integrity Failures </li>
<li> <a href="https://docs.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-security-guide?s=03">docs.microsoft.com</a> -
BinaryFormatter security guide </li>
<li> <a href="https://owasp.org/www-project-top-ten/2017/A8_2017-Insecure_Deserialization">OWASP Top 10 2017 Category A8</a> - Insecure
Deserialization </li>
<li> <a href="https://cwe.mitre.org/data/definitions/134">MITRE, CWE-134</a> - Use of Externally-Controlled Format String </li>
<li> <a href="https://cwe.mitre.org/data/definitions/502">MITRE, CWE-502</a> - Deserialization of Untrusted Data </li>
<li> <a href="https://www.sans.org/top25-software-errors/#cat2">SANS Top 25</a> - Risky Resource Management </li>
<li> <a href="https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Deserialization_Cheat_Sheet.md">OWASP Deserialization Cheat
Sheet</a> </li>
</ul>

33 changes: 33 additions & 0 deletions analyzers/rspec/vbnet/S5773.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"title": "Types allowed to be deserialized should be restricted",
"type": "VULNERABILITY",
"status": "ready",
"quickfix": "targeted",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "30min"
},
"tags": [
"cwe"
],
"defaultSeverity": "Major",
"ruleSpecification": "RSPEC-5773",
"sqKey": "S5773",
"scope": "Main",
"securityStandards": {
"CWE": [
502
],
"OWASP": [
"A8"
],
"OWASP Top 10 2021": [
"A8"
],
"ASVS 4.0": [
"1.5.2",
"5.5.1",
"5.5.3"
]
}
}
1 change: 1 addition & 0 deletions analyzers/rspec/vbnet/Sonar_way_profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
"S5659",
"S5693",
"S5753",
"S5773",
"S5856",
"S5944",
"S6145",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@ public override bool ShouldExecute()
return walker.Result;
}

protected override SyntaxNode FindBindToTypeMethodDeclaration(IOperation operation) =>
MethodCandidates(operation).FirstOrDefault(x =>
x is MethodDeclarationSyntax { Identifier.Text: nameof(SerializationBinder.BindToType), ParameterList: { Parameters.Count: 2 } } method
&& (method.Body is not null || method.ArrowExpressionBody() is not null)
&& method.EnsureCorrectSemanticModelOrDefault(SemanticModel) is { } semanticModel
&& method.ParameterList.Parameters[0].Type.IsKnownType(KnownType.System_String, semanticModel)
&& method.ParameterList.Parameters[1].Type.IsKnownType(KnownType.System_String, semanticModel));

protected override SyntaxNode FindResolveTypeMethodDeclaration(IOperation operation) =>
MethodCandidates(operation)?.FirstOrDefault(x =>
x is MethodDeclarationSyntax { Identifier.Text: "ResolveType", ParameterList: { Parameters.Count: 1 } } method
&& (method.Body is not null || method.ArrowExpressionBody() is not null)
&& method.ParameterList.EnsureCorrectSemanticModelOrDefault(SemanticModel) is { } semanticModel
&& method.ParameterList.Parameters[0].Type.IsKnownType(KnownType.System_String, semanticModel));

protected override bool ThrowsOrReturnsNull(SyntaxNode methodDeclaration) => ((MethodDeclarationSyntax)methodDeclaration).ThrowsOrReturnsNull();

protected override SyntaxToken GetIdentifier(SyntaxNode methodDeclaration) => ((MethodDeclarationSyntax)methodDeclaration).Identifier;

private static IEnumerable<SyntaxNode> MethodCandidates(IOperation operation) =>
operation.Type?.DeclaringSyntaxReferences.SelectMany(x => x.GetSyntax().ChildNodes());

private sealed class Walker : SafeCSharpSyntaxWalker
{
public bool Result { get; private set; }
Expand All @@ -53,21 +75,4 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node)
public override void VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) =>
Result = node.Type.NameIs("LosFormatter");
}

protected override bool IsBindToTypeMethod(SyntaxNode methodDeclaration) =>
methodDeclaration is MethodDeclarationSyntax { Identifier.Text: nameof(SerializationBinder.BindToType), ParameterList.Parameters.Count: 2 } syntax
&& (syntax.Body is not null || syntax.ArrowExpressionBody() is not null)
&& syntax.EnsureCorrectSemanticModelOrDefault(SemanticModel) is { } semanticModel
&& syntax.ParameterList.Parameters[0].Type.IsKnownType(KnownType.System_String, semanticModel)
&& syntax.ParameterList.Parameters[1].Type.IsKnownType(KnownType.System_String, semanticModel);

protected override bool IsResolveTypeMethod(SyntaxNode methodDeclaration) =>
methodDeclaration is MethodDeclarationSyntax { Identifier.Text: "ResolveType", ParameterList.Parameters.Count: 1 } syntax
&& (syntax.Body is not null || syntax.ArrowExpressionBody() is not null)
&& syntax.EnsureCorrectSemanticModelOrDefault(SemanticModel) is { } semanticModel
&& syntax.ParameterList.Parameters[0].Type.IsKnownType(KnownType.System_String, semanticModel);

protected override bool ThrowsOrReturnsNull(SyntaxNode methodDeclaration) => ((MethodDeclarationSyntax)methodDeclaration).ThrowsOrReturnsNull();

protected override SyntaxToken GetIdentifier(SyntaxNode methodDeclaration) => ((MethodDeclarationSyntax)methodDeclaration).Identifier;
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ public abstract class RestrictDeserializedTypesBase : SymbolicRuleCheck
private readonly Dictionary<ISymbol, SyntaxNode> unsafeMethodsForSymbols = new();
private readonly Dictionary<IOperation, SyntaxNode> unsafeMethodsForOperations = new();

protected abstract bool IsBindToTypeMethod(SyntaxNode methodDeclaration);
protected abstract bool IsResolveTypeMethod(SyntaxNode methodDeclaration);
protected abstract SyntaxNode FindBindToTypeMethodDeclaration(IOperation operation);
protected abstract SyntaxNode FindResolveTypeMethodDeclaration(IOperation operation);
protected abstract bool ThrowsOrReturnsNull(SyntaxNode methodDeclaration);
protected abstract SyntaxToken GetIdentifier(SyntaxNode methodDeclaration);

Expand Down Expand Up @@ -115,8 +115,7 @@ private bool UnsafeResolver(ProgramState state, IOperation operation, out Syntax
{
return true;
}
else if (DeclarationCandidates(operation)?.FirstOrDefault(IsResolveTypeMethod) is { } declaration
&& !ThrowsOrReturnsNull(declaration))
else if (FindResolveTypeMethodDeclaration(operation) is { } declaration && !ThrowsOrReturnsNull(declaration))
{
resolveTypeDeclaration = declaration;
return true;
Expand Down Expand Up @@ -172,14 +171,11 @@ private bool BinderIsSafe(ProgramState state, IAssignmentOperationWrapper assign
}
else
{
bindToTypeDeclaration = DeclarationCandidates(state.ResolveCaptureAndUnwrapConversion(assignment.Value)).FirstOrDefault(IsBindToTypeMethod);
bindToTypeDeclaration = FindBindToTypeMethodDeclaration(state.ResolveCaptureAndUnwrapConversion(assignment.Value));
return bindToTypeDeclaration is null || ThrowsOrReturnsNull(bindToTypeDeclaration);
}
}

private static IEnumerable<SyntaxNode> DeclarationCandidates(IOperation operation) =>
operation.Type?.DeclaringSyntaxReferences.SelectMany(x => x.GetSyntax().ChildNodes());

private SyntaxNode UnsafeMethodDeclaration(ProgramState state, IOperation operation)
{
operation = state.ResolveCaptureAndUnwrapConversion(operation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public SymbolicExecutionRunner() : base(AnalyzerConfiguration.AlwaysEnabled) { }
.Add(PublicMethodArgumentsShouldBeCheckedForNull.S3900, CreateFactory<PublicMethodArgumentsShouldBeCheckedForNull>())
.Add(CalculationsShouldNotOverflow.S3949, CreateFactory<CalculationsShouldNotOverflow>())
.Add(ObjectsShouldNotBeDisposedMoreThanOnce.S3966, CreateFactory<ObjectsShouldNotBeDisposedMoreThanOnce>())
.Add(EmptyCollectionsShouldNotBeEnumerated.S4158, CreateFactory<EmptyCollectionsShouldNotBeEnumerated>());
.Add(EmptyCollectionsShouldNotBeEnumerated.S4158, CreateFactory<EmptyCollectionsShouldNotBeEnumerated>())
.Add(RestrictDeserializedTypes.S5773, CreateFactory<RestrictDeserializedTypes>());

protected override SyntaxClassifierBase SyntaxClassifier => VisualBasicSyntaxClassifier.Instance;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* SonarAnalyzer for .NET
* Copyright (C) 2015-2023 SonarSource SA
* mailto: contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System.Runtime.Serialization;
using StyleCop.Analyzers.Lightup;

namespace SonarAnalyzer.SymbolicExecution.Roslyn.RuleChecks.VisualBasic;

public sealed class RestrictDeserializedTypes : RestrictDeserializedTypesBase
{
public static readonly DiagnosticDescriptor S5773 = DescriptorFactory.Create(DiagnosticId, MessageFormat);

protected override DiagnosticDescriptor Rule => S5773;

public override bool ShouldExecute()
{
var walker = new Walker();
walker.SafeVisit(Node);
return walker.Result;
}

protected override SyntaxNode FindBindToTypeMethodDeclaration(IOperation operation) =>
MethodCandidates(operation).FirstOrDefault(x =>
x is MethodBlockSyntax { SubOrFunctionStatement: { Identifier.Text: nameof(SerializationBinder.BindToType), ParameterList: { Parameters.Count: 2 } parameterList } }
&& parameterList.EnsureCorrectSemanticModelOrDefault(SemanticModel) is { } semanticModel
&& parameterList.Parameters[0].AsClause.Type.IsKnownType(KnownType.System_String, semanticModel)
&& parameterList.Parameters[1].AsClause.Type.IsKnownType(KnownType.System_String, semanticModel));

protected override SyntaxNode FindResolveTypeMethodDeclaration(IOperation operation) =>
MethodCandidates(operation)?.FirstOrDefault(x =>
x is MethodBlockSyntax { SubOrFunctionStatement: { Identifier.Text: "ResolveType", ParameterList: { Parameters.Count: 1 } parameterList } }
&& parameterList.EnsureCorrectSemanticModelOrDefault(SemanticModel) is { } semanticModel
&& parameterList.Parameters[0].AsClause.Type.IsKnownType(KnownType.System_String, semanticModel));

protected override bool ThrowsOrReturnsNull(SyntaxNode methodDeclaration) =>
methodDeclaration.DescendantNodes().OfType<ThrowStatementSyntax>().Any() ||
methodDeclaration.DescendantNodes().OfType<ExpressionSyntax>().Any(expression => expression.IsKind(SyntaxKindEx.ThrowExpression)) ||
methodDeclaration.DescendantNodes().OfType<ReturnStatementSyntax>().Any(returnStatement => returnStatement.Expression.IsKind(SyntaxKind.NothingLiteralExpression)) ||
// For simplicity this returns true for any method witch contains a NullLiteralExpression but this could be a source of FNs
methodDeclaration.DescendantNodes().OfType<ExpressionSyntax>().Any(expression => expression.IsKind(SyntaxKind.NothingLiteralExpression));

protected override SyntaxToken GetIdentifier(SyntaxNode methodDeclaration) => ((MethodBlockSyntax)methodDeclaration).SubOrFunctionStatement.Identifier;

private static IEnumerable<SyntaxNode> MethodCandidates(IOperation operation) =>
operation.Type?.DeclaringSyntaxReferences.SelectMany(x => x.GetSyntax().Parent.DescendantNodes());

private sealed class Walker : SafeVisualBasicSyntaxWalker
{
public bool Result { get; private set; }

public override void Visit(SyntaxNode node)
{
if (!Result)
{
base.Visit(node);
}
}

public override void VisitInvocationExpression(InvocationExpressionSyntax node) =>
Result = node.NameIs(nameof(IFormatter.Deserialize));

public override void VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) =>
Result = node.Type.NameIs("LosFormatter");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5697,7 +5697,7 @@ internal static class RuleTypeMappingVB
// ["S5770"],
// ["S5771"],
// ["S5772"],
// ["S5773"],
["S5773"] = "VULNERABILITY",
// ["S5774"],
// ["S5775"],
// ["S5776"],
Expand Down
Loading

0 comments on commit 14115ea

Please sign in to comment.