Two modes: for Developers and Auditors.
Detects various security vulnerability patterns: SQL Injection, Cross-Site Scripting (XSS), Cross-Site Request Forgery (CSRF), XML eXternal Entity Injection (XXE), etc.
Taint analysis to track user input data.
Analyzes .NET and .NET Core projects in a background (IntelliSense) or during a build.
Continuous Integration (CI) through MSBuild. For Unix CI runners please use VS2017 NuGet package. See Continuous Integration Builds section for instructions.
Works with Visual Studio 2015 or higher. Visual Studio Community, Professional and Enterprise editions are supported. Other editors that support Roslyn based analyzers like Rider or OmniSharp should work too.
Security Code Scan (SCS) can be installed as:
- Visual Studio extension. Use the link or open "Tools > Extensions and Updates..." Select "Online" in the tree on the left and search for SecurityCodeScan in the right upper field. Click "Download" and install.
- NuGet package.
- Right-click on the root item in your solution. Select "Manage NuGet Packages for Solution...". Select "Browse" on the top and search for Security Code Scan. Select project you want to install into and click "Install".
- Another option is to install the package into all projects in a solution: use "Tools > NuGet Package Manager > Package Manager Console". Run the command
Get-Project -All | Install-Package SecurityCodeScan
.
Installing it as NuGet package gives an advantage to choose projects in a solution that should be analyzed. It is a good idea to exclude test projects, because they do not make it into a final product.
⚠️ Note: In a .NET Core project, if you add a reference to a project that has SCS as a NuGet package, it is automatically added to the dependent project too. To disable this behavior, for example if the dependent project is a unit test project, mark the NuGet package as private in the .csproj or .vbproj file of the referenced project:<PackageReference Include="SecurityCodeScan" Version="3.0.0" PrivateAssets="all" />
However it requires discipline to install SCS into every solution a developer works with. Installing it as a Visual Studio extension is a single install action.
The NuGet version runs during a build and in background as IntelliSense (VS extension provides IntelliSense only) and can be integrated to any Continuous Integration (CI) server that supports MSBuild.
If the CI server of your choice is using MSBuild, then integration of SCS is just a matter of adding NuGet packages and collecting the output from the build. SCS warnings are in the form of
[source file](line,column): warning SCS[rule id]: [warning description] [project_file]
If your CI server doesn't support MSBuild, here is an example how it can be scripted to use Docker container for building:
git clone
or copy by other means the sources to a local directory.docker run -ti --rm --volume $PWD/SourcesFolderName:/tmp/app -w /tmp/app microsoft/dotnet:2.0-sdk
dotnet add src/SourcesFolderName/ProjectName.csproj package SecurityCodeScanVS2017
to reference SCS NuGet package in specific project file. Repeat for every project you want to analyze. Strictly speaking the step is not necessary if the SCS NuGet package is already referenced in project during development.dotnet build
- Grep the output.
Full solution analysis is a Visual Studio (2015 Update 3 RC and later) feature that enables you to choose whether you see code analysis issues only in open Visual C# or Visual Basic files in your solution, or in both open and closed Visual C# or Visual Basic files in your solution. For performance reasons it is disabled by default. It is not needed if SCS is installed as NuGet package, because it will run during a build, but if it is enabled you'll see the warnings as IntelliSense from NuGet too. In VS extension case open Tools > Options in Visual Studio. Select Text Editor > C# (or Basic) > Advanced. Make sure the "Enable full solution analysis" is checked:
Since Full solution analysis for IntelliSense has performance impact this is another reason to use SCS during a build only as a NuGet instead of Visual Studio extension. Microsoft has some additional information on the configuration option.
Download an intentionally vulnerable project WebGoat.NET for testing. Open the solution. If you have installed SCS as a VS extension you should see warning after few seconds in the "Errors" tab. Make sure IntelliSense results are not filtered in the window:
If SCS is installed as NuGet package and Full solution analysis is disabled you'll need to build the solution. Then you should see the warning in the "Errors" and "Output" tabs:
To enable analysis of these files you need to modify all C#(.csproj) and VB.NET(.vbproj) projects in a solution and add "AdditionalFileItemNames" element as shown below:
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
[..]
<TargetFrameworkProfile />
<!-- Add the line below -->
<AdditionalFileItemNames>$(AdditionalFileItemNames);Content</AdditionalFileItemNames>
</PropertyGroup>
The helper PowerShell script can be used to do it automatically for all projects in a subfolder:
Get-ChildItem *.csproj -Recurse | ForEach-Object {
$content = [xml] (Get-Content $_)
if (-not $content.Project.PropertyGroup[0].AdditionalFileItemNames)
{
Write-Host "AdditionalFileItemNames missing in $_"
$additionalFileItemNamesElt = $content.CreateElement("AdditionalFileItemNames",
"http://schemas.microsoft.com/developer/msbuild/2003")
$additionalFileItemNamesElt.set_InnerText('$(AdditionalFileItemNames);Content')
$content.Project.PropertyGroup[0].AppendChild($additionalFileItemNamesElt)
}
Set-ItemProperty $_ -name IsReadOnly -value $false
$content.Save($_)
# Normalize line endings
(Get-Content $_ -Encoding UTF8) | Set-Content $_ -Encoding UTF8
}
There are two types of external configuration files that can be used together: per user account and per project. It allows you to customize settings from built-in configuration or add new rules. Global settings file location is %LocalAppData%\SecurityCodeScan\config-2.1.yml
on Windows and $XDG_DATA_HOME/.local/share
on Unix.
For project specific settings add a file named SecurityCodeScan.config.yml into a project.
⚠️ Note: The file name doesn't have '2.1'. Instead it must haveVersion: 2.1
configuration setting in it's content. If the setting is missing you will get a runtime analysis exception.
Go to file properties and set the Build Action to AdditionalFiles:
An example of user's (per OS user) config-2.1.yml with custom Anti CSRF token:
CsrfProtection:
- Name: ASP.NET Core MVC
AntiCsrfAttributes:
- Name: MyNamespace.MyAntiCsrfAttribute
An example of SecurityCodeScan.config.yml (per project) with custom sink function (method that shouldn't be called with untrusted data without first being sanitized):
Version: 2.1
Behavior:
UniqueKey:
Namespace: MyNamespace
ClassName: Test
Name: VulnerableFunctionName
InjectableArguments: [SCS0001: [0: HtmlEscaped]]
See the configuration file for comments and examples of usage.
Audit mode is off by default. It can be turned on in an external configuration file to get more potentially false positive warnings about data with unknown taint state.
If Code Fixer is not implemented for the warning the link "Show potential fixes" won't work. For many warnings there are too many options to resolve the issue, so the code has to be modified manually. If the warning is false positive it can be suppressed that is standard functionality for Visual Studio however the UI not very intuitive, because you have to click on the underlined piece of code, only then a bubble appears at the beginning of the line where suppress menu is available:
Another place where the menu is available is Error List:
It is possible to filter shown item in Error List by different criteria: warning id, project name, etc. You can permanently suppress entire warning type for a project by setting it's warning id severity to None. Microsoft has it's own documentation about suppressions, rule sets and severities.
Each warning severity is configurable: expand References > Analyzers > SecurityCodeScan under the project in a Solution window, right click on a warning ID and modify the severity. WebGoat.NET.ruleset will be automatically saved in the project's directory:
If no SCS warnings are displayed, temporarily disable other installed analyzers. A buggy analyzer may affect results from other analyzers.
OWASP: Top 10 2013-A1-Injection
The dynamic value passed to the command execution should be validated.
If a malicious user controls either the FileName or Arguments, he might be able to execute unwanted commands or add unwanted argument. This behavior would not be possible if input parameter are validate against a white-list of characters.
var p = new Process();
p.StartInfo.FileName = "exportLegacy.exe";
p.StartInfo.Arguments = " -user " + input + " -role user";
p.Start();
Regex rgx = new Regex(@"^[a-zA-Z0-9]+$");
if(rgx.IsMatch(input))
{
var p = new Process();
p.StartInfo.FileName = "exportLegacy.exe";
p.StartInfo.Arguments = " -user " + input + " -role user";
p.Start();
}
OWASP: Command Injection
OWASP: Top 10 2013-A1-Injection
CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
The dynamic value passed to the XPath query should be validated.
If the user input is not properly filtered, a malicious user could extend the XPath query.
var doc = new XmlDocument {XmlResolver = null};
doc.Load("/config.xml");
var results = doc.SelectNodes("/Config/Devices/Device[id='" + input + "']");
Regex rgx = new Regex(@"^[a-zA-Z0-9]+$");
if(rgx.IsMatch(input)) //Additional validation
{
XmlDocument doc = new XmlDocument {XmlResolver = null};
doc.Load("/config.xml");
var results = doc.SelectNodes("/Config/Devices/Device[id='" + input + "']");
}
CWE-643: Improper Neutralization of Data within XPath Expressions ('XPath Injection')
OWASP: XPATH Injection
Black Hat Europe 2012: Hacking XPath 2.0
WASC-39: XPath Injection
The XML parser is configured incorrectly. The operation could be vulnerable to XML eXternal Entity (XXE) processing.
Prior to .NET 4.5.2
// DTD expansion is enabled by default
XmlReaderSettings settings = new XmlReaderSettings();
XmlReader reader = XmlReader.Create(inputXml, settings);
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(pathToXmlFile);
Console.WriteLine(xmlDoc.InnerText);
Prior to .NET 4.5.2
var settings = new XmlReaderSettings();
// Prior to .NET 4.0
settings.ProhibitDtd = true; // default is false!
// .NET 4.0 - .NET 4.5.2
settings.DtdProcessing = DtdProcessing.Prohibit; // default is DtdProcessing.Parse!
XmlReader reader = XmlReader.Create(inputXml, settings);
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.XmlResolver = null; // Setting this to NULL disables DTDs - Its NOT null by default.
xmlDoc.Load(pathToXmlFile);
Console.WriteLine(xmlDoc.InnerText);
.NET 4.5.2 and later
In .NET Framework versions 4.5.2 and up, XmlTextReader's internal XmlResolver is set to null by default, making the XmlTextReader ignore DTDs by default. The XmlTextReader can become unsafe if if you create your own non-null XmlResolver with default or unsafe settings.
OWASP.org: XML External Entity (XXE) Prevention Cheat Sheet (.NET)
CWE-611: Improper Restriction of XML External Entity Reference ('XXE')
CERT: IDS10-J. Prevent XML external entity attacks
OWASP.org: XML External Entity (XXE) Processing
WS-Attacks.org: XML Entity Expansion
WS-Attacks.org: XML External Entity DOS
WS-Attacks.org: XML Entity Reference Attack
Identifying Xml eXternal Entity vulnerability (XXE)
A path traversal attack (also known as directory traversal) aims to access files and directories that are stored outside the expected directory.By manipulating variables that reference files with “dot-dot-slash (../)” sequences and its variations or by using absolute file paths, it may be possible to access arbitrary files and directories stored on file system including application source code or configuration and critical system files.
With a malicious relative path, an attacker could reach a secret file.
[RedirectingAction]
public ActionResult Download(string fileName)
{
byte[] fileBytes = System.IO.File.ReadAllBytes(Server.MapPath("~/ClientDocument/") + fileName);
return File(fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet, fileName);
}
The following request downloads a file of the attacker choice:
http://www.address.com/Home/Download?fileName=../../somefile.txt
Do not try to strip invalid characters. Fail if any unexpected character is detected.
private static readonly char[] InvalidFilenameChars = Path.GetInvalidFileNameChars();
[RedirectingAction]
public ActionResult Download(string fileName)
{
if (fileName.IndexOfAny(InvalidFilenameChars) >= 0)
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
byte[] fileBytes = System.IO.File.ReadAllBytes(Server.MapPath("~/ClientDocument/") + fileName);
return File(fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet, fileName);
}
If the input is not supplied by user or a validation is in place the warning can be suppressed.
OWASP: Path Traversal
OS Command Injection, Path Traversal & Local File Inclusion Vulnerability - Notes
A potential XSS was found. The endpoint returns a variable from the client input that has not been encoded. To protect against stored XSS attacks, make sure any dynamic content coming from user or data store cannot be used to inject JavaScript on a page. Most modern frameworks will escape dynamic content by default automatically (Razor for example) or by using special syntax (<%: content %>
, <%= HttpUtility.HtmlEncode(content) %>
).
XSS could be used to execute unwanted JavaScript in a client's browser. XSS can be used to steal the cookie containing the user’s session ID. There is rarely a good reason to read or manipulate cookies in client-side JavaScript, so consider marking cookies as HTTP-only, meaning that cookies will be received, stored, and sent by the browser, but cannot be modified or read by JavaScript.
public class TestController : Controller
{
[HttpGet(""{myParam}"")]
public string Get(string myParam)
{
return "value " + myParam;
}
}
public class TestController : Controller
{
[HttpGet(""{myParam}"")]
public string Get(string myParam)
{
return "value " + HttpUtility.HtmlEncode(myParam);
}
}
WASC-8: Cross Site Scripting
OWASP: XSS Prevention Cheat Sheet
OWASP: Top 10 2013-A3: Cross-Site Scripting (XSS)
CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The dynamic value passed to the LDAP query should be validated.
If the user input is not properly filtered, a malicious user could extend the LDAP query.
var searcher = new DirectorySearcher();
searcher.Filter = "(cn=" + input + ")";
or
var dir = new DirectoryEntry();
dir.Path = $"GC://DC={input},DC=com";
Use proper encoder (LdapFilterEncode
or LdapDistinguishedNameEncode
) from AntiXSS library:
var searcher = new DirectorySearcher();
searcher.Filter = "(cn=" + Encoder.LdapFilterEncode(input) + ")";
or
var dir = new DirectoryEntry();
dir.Path = $"GC://DC={Encoder.LdapDistinguishedNameEncode(input)},DC=com";
OWASP: LDAP Injection
OWASP: LDAP Injection Prevention Cheat Sheet
MSDN Blog - Security Tools: LDAP Injection and mitigation
WASC-29: LDAP Injection
CWE-90: Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection')
SQL injection flaws are introduced when software developers create dynamic database queries that include user supplied input.
Malicious user might get direct read and/or write access to the database. If the database is poorly configured the attacker might even get Remote Code Execution (RCE) on the machine running the database.
WASC-19: SQL Injection
OWASP: SQL Injection Prevention Cheat Sheet
OWASP: Query Parameterization Cheat Sheet
CAPEC-66: SQL Injection
CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
db.ExecuteQuery(@"SELECT name FROM dbo.Users WHERE UserId = " + inputId + " AND group = 5");
var query = "SELECT name FROM dbo.Users WHERE UserId = " + userId + " AND group = 5";
var id = context.ExecuteQuery<IEnumerable<string>>(query).SingleOrDefault();
var query = from user in db.Users
where user.UserId == inputId
select user.name;
var query = "SELECT name FROM dbo.Users WHERE UserId = {0} AND group = 5";
var id = context.ExecuteQuery<IEnumerable<string>>(query, userId).SingleOrDefault();
LINQ: How to Query for Information
Unsafe usage of System.Web.UI.WebControls.SqlDataSource, System.Web.UI.WebControls.SqlDataSourceView or Microsoft.Whos.Framework.Data.SqlUtility.
"Select * From Customers where CustomerName = " & txtCustomerName.Value
To help protect against SQL statement exploits, never create SQL queries using string concatenation. Instead, use a parameterized query and assign user input to parameter objects. By default, the SqlDataSource control uses the System.Data.SqlClient data provider to work with SQL Server as the data source. The System.Data.SqlClient provider supports named parameters as placeholders, as shown in the following example:
<asp:sqlDataSource ID="EmployeeDetailsSqlDataSource"
SelectCommand="SELECT EmployeeID, LastName, FirstName FROM Employees WHERE EmployeeID = @EmpID"
InsertCommand="INSERT INTO Employees(LastName, FirstName) VALUES (@LastName, @FirstName);
SELECT @EmpID = SCOPE_IDENTITY()"
UpdateCommand="UPDATE Employees SET LastName=@LastName, FirstName=@FirstName
WHERE EmployeeID=@EmployeeID"
DeleteCommand="DELETE Employees WHERE EmployeeID=@EmployeeID"
ConnectionString="<%$ ConnectionStrings:NorthwindConnection %>"
OnInserted="EmployeeDetailsSqlDataSource_OnInserted"
RunAt="server">
<SelectParameters>
<asp:Parameter Name="EmpID" Type="Int32" DefaultValue="0" />
</SelectParameters>
<InsertParameters>
<asp:Parameter Name="EmpID" Direction="Output" Type="Int32" DefaultValue="0" />
</InsertParameters>
</asp:sqlDataSource>
If you are connecting to an OLE DB or ODBC data source, you can configure the SqlDataSource control to use the System.Data.OleDb or System.Data.Odbc provider to work with your data source, respectively. The System.Data.OleDb and System.Data.Odbc providers support only positional parameters identified by the "?" character, as shown in the following example:
...
<asp:SqlDataSource ID="EmployeeDetailsSqlDataSource"
SelectCommand="SELECT EmployeeID, LastName, FirstName, Address, City, Region, PostalCode
FROM Employees WHERE EmployeeID = ?"
InsertCommand="INSERT INTO Employees(LastName, FirstName, Address, City, Region, PostalCode)
VALUES (?, ?, ?, ?, ?, ?);
SELECT @EmpID = SCOPE_IDENTITY()"
UpdateCommand="UPDATE Employees SET LastName=?, FirstName=?, Address=?,
City=?, Region=?, PostalCode=?
WHERE EmployeeID=?"
...
MSDN: Using Parameters with the SqlDataSource Control
MSDN: Script Exploits Overview
MSDN: Filtering Event
See references in the main SQL Injection section
Use parametrized queries to mitigate SQL injection.
string queryString = "SELECT OrderID, CustomerID FROM Orders WHERE OrderId = " + userInput;
using (var connection = new OleDbConnection(connectionString))
{
OleDbCommand command = new OleDbCommand(queryString, connection);
connection.Open();
OleDbDataReader reader = command.ExecuteReader();
}
string queryString = "SELECT OrderID, CustomerID FROM Orders WHERE OrderId = ?";
using (var connection = new OleDbConnection(connectionString))
{
OleDbCommand command = new OleDbCommand(queryString, connection);
command.Parameters.Add("@p1", OleDbType.Integer).Value = userInput;
connection.Open();
OleDbDataReader reader = command.ExecuteReader();
}
OleDbCommand Documentation
See references in the main SQL Injection section
Use parametrized queries to mitigate SQL injection.
var command = new OdbcCommand("SELECT * FROM [user] WHERE id = " + userInput, connection);
OdbcDataReader reader = command.ExecuteReader();
var command = new OdbcCommand("SELECT * FROM [user] WHERE id = ?", connection);
command.Parameters.Add("@id", OdbcType.Int).Value = 4;
OdbcDataReader reader = command.ExecuteReader();
OdbcCommand Documentation
See references in the main SQL Injection section
Use parametrized queries to mitigate SQL injection.
var cmd = new SqlCommand("SELECT * FROM Users WHERE username = '" + username + "' and role='user'");
var cmd = new SqlCommand("SELECT * FROM Users WHERE username = @username and role='user'");
cmd.Parameters.AddWithValue("username", username);
SqlCommand Class Documentation
See references in the main SQL Injection section
Use parametrized queries to mitigate SQL injection.
var cmd = "SELECT * FROM Users WHERE username = '" + input + "' and role='user'";
ctx.Database.ExecuteSqlCommand(
cmd);
var cmd = "SELECT * FROM Users WHERE username = @username and role='user'";
ctx.Database.ExecuteSqlCommand(
cmd,
new SqlParameter("@username", input));
Entity Framework Documentation
Execute Raw SQL Queries in Entity Framework 6
Executing Raw SQL Queries in Entity Framework Core
Bobby Tables: A guide to preventing SQL injection > Entity Framework
See references in the main SQL Injection section
Use parametrized queries to mitigate SQL injection.
db.ExecuteDataSet(CommandType.Text, "SELECT * FROM Users WHERE username = '" + input + "' and role='user'");
DbCommand cmd = db.GetSqlStringCommand("SELECT * FROM Users WHERE username = @username and role='user'");
db.AddInParameter(cmd, "@username", DbType.String, input);
db.ExecuteDataSet(cmd);
Microsoft.Practices.EnterpriseLibrary.Data.Sql Namespace
See references in the main SQL Injection section
Certificate Validation has been disabled. The communication could be intercepted.
Disabling certificate validation is often used to connect easily to a host that is not signed by a root certificate authority. As a consequence, this is vulnerable to Man-in-the-middle attacks since the client will trust any certificate.
ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
- Make sure the validation is disabled only in testing environment or
- Use certificate pinning for development or
- Use properly signed certificates for development
#if DEBUG
ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
#endif
WASC-04: Insufficient Transport Layer Protection
CWE-295: Improper Certificate Validation
The random numbers generated could be predicted.
The use of a predictable random value can lead to vulnerabilities when used in certain security critical contexts.
var rnd = new Random();
byte[] buffer = new byte[16];
rnd.GetBytes(buffer);
return BitConverter.ToString(buffer);
using System.Security.Cryptography;
var rnd = RandomNumberGenerator.Create();
OWASP: Insecure Randomness
CWE-330: Use of Insufficiently Random Values
CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
CWE-331: Insufficient Entropy
MD5 or SHA1 have known collision weaknesses and are no longer considered strong hashing algorithms.
var hashProvider = new SHA1CryptoServiceProvider();
var hash = hashProvider.ComputeHash(str);
Use SHA256 or SHA512. Notice, that hashing algorithms are designed to be fast and shouldn't be used directly for hashing passwords. Use adaptive algorithms for the purpose.
var hashProvider = SHA256Managed.Create();
var hash = hashProvider.ComputeHash(str);
MSDN: SHA256 Class documentation
Salted Password Hashing - Doing it Right
DES and 3DES are not considered a strong cipher for modern applications. Currently, NIST recommends the usage of AES block ciphers instead.
Broken or deprecated ciphers have typically known weakness. A attacker might be able to brute force the secret key use for the encryption. The confidentiality and integrity of the information encrypted is at risk.
DES DESalg = DES.Create();
// Create a string to encrypt.
byte[] encrypted;
ICryptoTransform encryptor = DESalg.CreateEncryptor(key, zeroIV);
// Create the streams used for encryption.
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt,
encryptor,
CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
//Write all data to the stream.
swEncrypt.Write(Data);
}
encrypted = msEncrypt.ToArray();
return encrypted;
}
}
Use AES for symmetric encryption.
// Create a string to encrypt.
byte[] encrypted;
var encryptor = new AesManaged();
encryptor.Key = key;
encryptor.GenerateIV();
var iv = encryptor.IV;
// Create the streams used for encryption.
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt,
encryptor.CreateEncryptor(),
CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
//Write all data to the stream.
swEncrypt.Write(Data);
}
encrypted = msEncrypt.ToArray();
return encrypted;
}
}
Notice that AES itself doesn't protect from encrypted data tampering. For an example of authenticated encryption see the Solution in Weak Cipher Mode
NIST Withdraws Outdated Data Encryption Standard
CWE-326: Inadequate Encryption Strength
StackOverflow: Authenticated encryption example
The CBC mode alone is susceptible to padding oracle attack.
If an attacker is able to submit encrypted payload and the server is decrypting its content. The attacker is likely to decrypt its content.
using (var aes = new AesManaged {
KeySize = KeyBitSize,
BlockSize = BlockBitSize,
Mode = CipherMode.CBC,
Padding = PaddingMode.PKCS7
})
{
//Use random IV
aes.GenerateIV();
iv = aes.IV;
using (var encrypter = aes.CreateEncryptor(cryptKey, iv))
using (var cipherStream = new MemoryStream())
{
using (var cryptoStream = new CryptoStream(cipherStream, encrypter, CryptoStreamMode.Write))
using (var binaryWriter = new BinaryWriter(cryptoStream))
{
//Encrypt Data
binaryWriter.Write(secretMessage);
}
cipherText = cipherStream.ToArray();
}
}
//No HMAC suffix to check integrity!!!
See the Solution in Weak Cipher Mode.
Padding Oracles for the masses (by Matias Soler)
Wikipedia: Authenticated encryption
NIST: Authenticated Encryption Modes
CAPEC: Padding Oracle Crypto Attack
CWE-696: Incorrect Behavior Order
ECB mode will produce the same result for identical blocks (ie: 16 bytes for AES). An attacker could be able to guess the encrypted message. The use of AES in CBC mode with a HMAC is recommended guaranteeing integrity and confidentiality.
The ECB mode will produce identical encrypted block for equivalent plain text block. This could allow an attacker that is eavesdropping to guess the content sent. This same property can also allow the recovery of the original message. Furthermore, this cipher mode alone does not guarantee integrity.
using (var aes = new AesManaged {
KeySize = KeyBitSize,
BlockSize = BlockBitSize,
Mode = CipherMode.ECB, // !!!
Padding = PaddingMode.PKCS7
})
{
//Use random IV
aes.GenerateIV();
iv = aes.IV;
using (var encrypter = aes.CreateEncryptor(cryptKey, iv))
using (var cipherStream = new MemoryStream())
{
using (var cryptoStream = new CryptoStream(cipherStream, encrypter, CryptoStreamMode.Write))
using (var binaryWriter = new BinaryWriter(cryptoStream))
{
//Encrypt Data
binaryWriter.Write(secretMessage);
}
cipherText = cipherStream.ToArray();
}
}
//No HMAC suffix to check integrity!!!
Use some other mode, but notice that CBC without authenticated integrity check is vulnerable to another type of attack. For an example of authenticated integrity check see the Solution in Weak Cipher Mode.
Wikipedia: ECB mode
Padding Oracles for the masses (by Matias Soler)
Wikipedia: Authenticated encryption
NIST: Authenticated Encryption Modes
CAPEC: Padding Oracle Crypto Attack
CWE-696: Incorrect Behavior Order
The cipher text produced is susceptible to alteration by an adversary.
The cipher provides no way to detect that the data has been tampered with. If the cipher text can be controlled by an attacker, it could be altered without detection. The use of AES in CBC mode with a HMAC is recommended guaranteeing integrity and confidentiality.
using (var aes = new AesManaged {
KeySize = KeyBitSize,
BlockSize = BlockBitSize,
Mode = CipherMode.OFB,
Padding = PaddingMode.PKCS7
})
{
using (var encrypter = aes.CreateEncryptor(cryptKey, new byte[16]))
using (var cipherStream = new MemoryStream())
{
using (var cryptoStream = new CryptoStream(cipherStream, encrypter, CryptoStreamMode.Write))
using (var binaryWriter = new BinaryWriter(cryptoStream))
{
//Encrypt Data
binaryWriter.Write(secretMessage);
}
cipherText = cipherStream.ToArray();
}
}
//Missing HMAC suffix to assure integrity
Using bouncy castle:
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
public static readonly int BlockBitSize = 128;
public static readonly int KeyBitSize = 256;
public static byte[] SimpleEncrypt(byte[] secretMessage, byte[] key)
{
//User Error Checks
if (key == null || key.Length != KeyBitSize / 8)
throw new ArgumentException(String.Format("Key needs to be {0} bit!", KeyBitSize), "key");
if (secretMessage == null || secretMessage.Length == 0)
throw new ArgumentException("Secret Message Required!", "secretMessage");
//Using random nonce large enough not to repeat
var nonce = new byte[NonceBitSize / 8];
Random.NextBytes(nonce, 0, nonce.Length);
var cipher = new GcmBlockCipher(new AesFastEngine());
var parameters = new AeadParameters(new KeyParameter(key), MacBitSize, nonce, new byte[0]);
cipher.Init(true, parameters);
//Generate Cipher Text With Auth Tag
var cipherText = new byte[cipher.GetOutputSize(secretMessage.Length)];
var len = cipher.ProcessBytes(secretMessage, 0, secretMessage.Length, cipherText, 0);
cipher.DoFinal(cipherText, len);
//Assemble Message
using (var combinedStream = new MemoryStream())
{
using (var binaryWriter = new BinaryWriter(combinedStream))
{
//Prepend Nonce
binaryWriter.Write(nonce);
//Write Cipher Text
binaryWriter.Write(cipherText);
}
return combinedStream.ToArray();
}
}
Custom implementation of Encrypt and HMAC:
using System.IO;
using System.Security.Cryptography;
public static byte[] SimpleEncrypt(byte[] secretMessage, byte[] cryptKey, byte[] authKey, byte[] nonSecretPayload = null)
{
//User Error Checks
if (cryptKey == null || cryptKey.Length != KeyBitSize / 8)
throw new ArgumentException(String.Format("Key needs to be {0} bit!", KeyBitSize), "cryptKey");
if (authKey == null || authKey.Length != KeyBitSize / 8)
throw new ArgumentException(String.Format("Key needs to be {0} bit!", KeyBitSize), "authKey");
if (secretMessage == null || secretMessage.Length < 1)
throw new ArgumentException("Secret Message Required!", "secretMessage");
byte[] cipherText;
byte[] iv;
using (var aes = new AesManaged {
KeySize = KeyBitSize,
BlockSize = BlockBitSize,
Mode = CipherMode.CBC,
Padding = PaddingMode.PKCS7
})
{
//Use random IV
aes.GenerateIV();
iv = aes.IV;
using (var encrypter = aes.CreateEncryptor(cryptKey, iv))
using (var cipherStream = new MemoryStream())
{
using (var cryptoStream = new CryptoStream(cipherStream, encrypter, CryptoStreamMode.Write))
using (var binaryWriter = new BinaryWriter(cryptoStream))
{
//Encrypt Data
binaryWriter.Write(secretMessage);
}
cipherText = cipherStream.ToArray();
}
}
//Assemble encrypted message and add authentication
using (var hmac = new HMACSHA256(authKey))
using (var encryptedStream = new MemoryStream())
{
using (var binaryWriter = new BinaryWriter(encryptedStream))
{
//Prepend IV
binaryWriter.Write(iv);
//Write Ciphertext
binaryWriter.Write(cipherText);
binaryWriter.Flush();
//Authenticate all data
var tag = hmac.ComputeHash(encryptedStream.ToArray());
//Postpend tag
binaryWriter.Write(tag);
}
return encryptedStream.ToArray();
}
}
Padding Oracles for the masses (by Matias Soler)
Wikipedia: Authenticated encryption
NIST: Authenticated Encryption Modes
CAPEC: Padding Oracle Crypto Attack
CWE-696: Incorrect Behavior Order
It is recommended to specify the Secure flag to new cookie.
The Secure flag is a directive to the browser to make sure that the cookie is not sent by unencrypted channel
The requireSSL
value is explicitly set to false
or the default is left.
<httpCookies requireSSL="false" [..] />
// default is left
var cookie = new HttpCookie("test");
// or explicitly set to false
var cookie = new HttpCookie("test");
cookie.Secure = false;
<httpCookies requireSSL="true" [..] />
var cookie = new HttpCookie("test");
cookie.Secure = true; //Add this flag
cookie.HttpOnly = true;
CWE-614: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute
CWE-315: Cleartext Storage of Sensitive Information in a Cookie
CWE-311: Missing Encryption of Sensitive Data
OWASP: Secure Flag
Rapid7: Missing Secure Flag From SSL Cookie
It is recommended to specify the HttpOnly flag to new cookie.
Cookies that doesn't have the flag set are available to JavaScript running on the same domain. When a user is the target of a "Cross-Site Scripting", the attacker would benefit greatly from getting the session id.
The httpOnlyCookies
value is explicitly set to false
or the default is left.
<httpCookies httpOnlyCookies="false" [..] />
// default is left
var cookie = new HttpCookie("test");
// or explicitly set to false
var cookie = new HttpCookie("test");
cookie.HttpOnly = false;
<httpCookies httpOnlyCookies="true" [..] />
var cookie = new HttpCookie("test");
cookie.Secure = true;
cookie.HttpOnly = true; //Add this flag
Coding Horror blog: Protecting Your Cookies: HttpOnly
OWASP: HttpOnly
Rapid7: Missing HttpOnly Flag From Cookie
The viewStateEncryptionMode
is not set to Always
in configuration file.
Web Forms controls use hidden base64 encoded fields to store state information. If sensitive information is stored there it may be leaked to the client side.
The default value is Auto
:
<system.web>
...
<pages [..] viewStateEncryptionMode="Auto" [..]/>
...
</system.web>
or
<system.web>
...
<pages [..] viewStateEncryptionMode="Never" [..]/>
...
</system.web>
Explicitly set to Always
and encrypt with with the .NET machine key:
<system.web>
...
<pages [..] viewStateEncryptionMode="Always" [..]/>
...
</system.web>
MSDN: pages Element (ASP.NET Settings Schema)
MSDN: ViewStateEncryptionMode Property
MSDN: machineKey Element (ASP.NET Settings Schema)
The enableViewStateMac
is disabled in configuration file. (This feature cannot be disabled starting .NET 4.5.1)
The view state could be altered by an attacker.
<system.web>
...
<pages [..] enableViewStateMac="false" [..]/>
...
</system.web>
The default value is secure - true
.
Or set it explicitly:
<system.web>
...
<pages [..] enableViewStateMac="true" [..]/>
...
</system.web>
MSDN: pages Element (ASP.NET Settings Schema)
Request validation is disabled. Request validation allows the filtering of some XSS patterns submitted to the application.
public class TestController
{
[HttpPost]
[ValidateInput(false)]
public ActionResult ControllerMethod(string input) {
return f(input);
}
}
Although it performs blacklisting (that is worse than whitelisting by definition) and you should not rely solely on it for XSS protection, it provides a first line of defense for your application. Do not disable the validation:
public class TestController
{
[HttpPost]
public ActionResult ControllerMethod(string input) {
return f(input);
}
}
Always user proper encoder (Html, Url, etc.) before displaying or using user supplied data (even if it is loaded from database).
MSDN: Request Validation in ASP.NET
OWASP: ASP.NET Request Validation
See XSS references.
The validateRequest
which provides additional protection against XSS is disabled in configuration file.
<system.web>
...
<pages [..] validateRequest="false" [..]/>
...
</system.web>
Although it performs blacklisting (that is worse than whitelisting by definition) and you should not rely solely on it for XSS protection, it provides a first line of defense for your application. Do not disable the validation:
The default value is true
.
Or set it explicitly:
<system.web>
...
<pages [..] validateRequest="true" [..]/>
...
</system.web>
MSDN: pages Element (ASP.NET Settings Schema)
MSDN: Request Validation in ASP.NET
OWASP: ASP.NET Request Validation
See XSS references.
The requestValidationMode
which provides additional protection against XSS is enabled only for pages, not for all HTTP requests in configuration file.
<system.web>
...
<httpRuntime [..] requestValidationMode="2.0" [..]/>
...
</system.web>
<system.web>
...
<httpRuntime [..] requestValidationMode="4.5" [..]/>
...
</system.web>
MSDN: pages Element (ASP.NET Settings Schema)
MSDN: Request Validation in ASP.NET
OWASP: ASP.NET Request Validation
MSDN: RequestValidationMode Property
See XSS references.
The password configuration to this API appears to be hardcoded.
If hard-coded passwords are used, it is almost certain that malicious users will gain access through the account in question.
config.setPassword("NotSoSecr3tP@ssword");
It is recommended to externalize configuration such as password to avoid leakage of secret information. The source code or its binary form is more likely to be accessible by an attacker than a production configuration. To be managed safely, passwords and secret keys should be stored encrypted in separate configuration files. The certificate for decryption should be installed as non-exportable on the server machine.
Configuration file :
<configuration>
<appSettings>
<add key="api_password" value="b3e521073ca276dc2b7caf6247b6ddc72d5e2d2d" />
</appSettings>
</configuration>
Code:
string apiPassword = ConfigurationManager.AppSettings["api_password"];
config.setPassword(apiPassword);
CWE-259: Use of Hard-coded Password
The RequiredLength property must be set with a minimum value of 8.
Weak password can be guessed or brute-forced.
ASP.NET Identity default is 6.
PasswordValidator pwdv = new PasswordValidator();
See the solution for Password Complexity
MSDN: ASP.NET Identity PasswordValidator Class
The minimal length of a password is recommended to be set at least to 8.
Weak password can be guessed or brute-forced.
PasswordValidator pwdv = new PasswordValidator
{
RequiredLength = 6,
};
See the solution for Password Complexity
MSDN: ASP.NET Identity PasswordValidator Class
PasswordValidator should have at least two requirements for better security (RequiredLength, RequireDigit, RequireLowercase, RequireUppercase and/or RequireNonLetterOrDigit).
Weak password can be guessed or brute-forced.
PasswordValidator pwdv = new PasswordValidator
{
RequiredLength = 6,
};
PasswordValidator pwdv = new PasswordValidator
{
RequiredLength = 8,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
MSDN: ASP.NET Identity PasswordValidator Class
Anti-forgery token is missing.
An attacker could send a link to the victim. By visiting the malicious link, a web page would trigger a POST request (because it is a blind attack - the attacker doesn't see a response from triggered request and has no use from GET request and GET requests should not change a state on the server by definition) to the website. The victim would not be able to acknowledge that an action is made in the background, but his cookie would be automatically submitted if he is authenticated to the website. This attack does not require special interaction other than visiting a website.
public class TestController
{
[HttpPost]
public ActionResult ControllerMethod(string input)
{
//Do an action in the context of the logged in user
}
}
In your view:
@Html.AntiForgeryToken()
In your controller:
public class TestController
{
[HttpPost]
[ValidateAntiForgeryToken] //Annotation added
public ActionResult ControllerMethod(string input)
{
//Do an action in the context of the logged in user
}
}
OWASP: Cross-Site Request Forgery
OWASP: CSRF Prevention Cheat Sheet
Caching conflicts with authorization.
Having the annotation [OutputCache] will disable the annotation [Authorize] for the requests following the first one.
[Authorize]
public class AdminController : Controller
{
[OutputCache]
public ActionResult Index()
{
return View();
}
}
Remove the caching:
[Authorize]
public class AdminController : Controller
{
public ActionResult Index()
{
return View();
}
}
Improving Performance with Output Caching
The enableEventValidation
is disabled in configuration file.
This feature reduces the risk of unauthorized or malicious post-back requests and callbacks. It is strongly recommended that you do not disable event validation. When the EnableEventValidation property is set to true, ASP.NET validates that a control event originated from the user interface that was rendered by that control.
<system.web>
...
<pages [..] enableEventValidation="false" [..]/>
...
</system.web>
The default value is secure - true
.
Or set it explicitly:
<system.web>
...
<pages [..] enableEventValidation="true" [..]/>
...
</system.web>
MSDN: pages Element (ASP.NET Settings Schema)
MSDN: Page.EnableEventValidation Property
The dynamic value passed to the Redirect
should be validated.
Your site may be used in phishing attacks. An attacker may craft a trustworthy looking link to your site redirecting a victim to a similar looking malicious site: https://www.yourdomain.com/loginpostback?redir=https://www.urdomain.com/login
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.UserName, model.Password))
{
FormsService.SignIn(model.UserName, model.RememberMe);
if (!String.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.UserName, model.Password))
{
FormsService.SignIn(model.UserName, model.RememberMe);
if (Url.IsLocalUrl(returnUrl)) // Make sure the url is relative, not absolute path
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
Microsoft: Preventing Open Redirection Attacks (C#)
OWASP: Unvalidated Redirects and Forwards Cheat Sheet
Hacksplaining: preventing malicious redirects
Untrusted data passed for deserialization.
Arbitrary code execution, full application compromise or denial of service. An attacker may pass specially crafted serialized .NET object of specific class that will execute malicious code during the construction of the object.
private void ConvertData(string json)
{
var mySerializer = new JavaScriptSerializer(new SimpleTypeResolver());
Object mything = mySerializer.Deserialize(json, typeof(SomeDataClass)/* the type doesn't matter */);
}
There is no simple fix. Do not deserialize untrusted data: user input, cookies or data that crosses trust boundaries.
In case it is unavoidable:
- If serialization is done on the server side, then crosses trust boundary, but is not modified and is returned back (like cookie for example) - use signed cryptography (HMAC for instance) to ensure it wasn't tampered.
- Do not get the type to deserialize into from untrusted source: the serialized stream itself or other untrusted parameter.
BinaryFormatter
for example reads type information from serialized stream itself and can't be used with untrusted streams:
// DO NOT DO THIS!
var thing = (MyType)new BinaryFormatter().Deserialize(untrustedStream);
JavaScriptSerializer for instance without a JavaScriptTypeResolver is safe because it doesn’t resolve types at all:
private void ConvertData(string json)
{
var mySerializer = new JavaScriptSerializer(/* no resolver here */);
Object mything = mySerializer.Deserialize(json, typeof(SomeDataClass));
}
Pass the expected type (may be hardcoded) to the deserialization library. Some libraries like Json.Net, DataContractJsonSerializer and FSPickler validate expected object graph before deserialization.
However the check is not bulletproof if the expected type contains field or property of System.Object
type somewhere nested in hierarchy.
// Json.net will inspect if the serialized data is the Expected type
var data = JsonConvert.DeserializeObject<Expected>(json, new
JsonSerializerSettings
{
// Type information is not used, only simple types like int, string, double, etc. will be resolved
TypeNameHandling = TypeNameHandling.None
});
// DO NOT DO THIS! The cast to MyType happens too late, when malicious code was already executed
var thing = (MyType)new BinaryFormatter().Deserialize(untrustedStream);
- If the library supports implement a callback that verifies if the object and its properties are of expected type (don't blacklist, use whitelist!):
class LimitedBinder : SerializationBinder
{
List<Type> allowedTypes = new List<Type>()
{
typeof(Exception),
typeof(List<Exception>),
};
public override Type BindToType(string assemblyName, string typeName)
{
var type = Type.GetType(String.Format("{0}, {1}", typeName, assemblyName), true);
foreach(Type allowedType in allowedTypes)
{
if(type == allowedType)
return allowedType;
}
// Don’t return null for unexpected types –
// this makes some serializers fall back to a default binder, allowing exploits.
throw new Exception("Unexpected serialized type");
}
}
var formatter = new BinaryFormatter() { Binder = new LimitedBinder () };
var data = (List<Exception>)formatter.Deserialize (fs);
Determining which types are safe is quite difficult, and this approach is not recommended unless necessary. There are many types that might allow non Remote Code Execution exploits if they are deserialized from untrusted data. Denial of service is especially common. As an example, the System.Collections.HashTable class is not safe to deserialize from an untrusted stream – the stream can specify the size of the internal “bucket” array and cause an out of memory condition.
- Serialize simple Data Transfer Objects (DTO) only. Do not serialize/deserialize type information. For example, use only
TypeNameHandling.None
(the default) in Json.net:
class DataForStorage
{
public string Id;
public int Count;
}
var data = JsonConvert.SerializeObject<DataForStorage>(json, new
JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.None
});
will produce the following JSON without type information that is perfectly fine to deserialize back:
{
"Id": null,
"Count": 0
}
BlackHat USA 2017: Friday the 13th: JSON Attacks
BlueHat v17: Dangerous Contents - Securing .Net Deserialization
BlackHat USA 2012: Are you my type?
OWASP: Deserialization of untrusted data
Deserialization payload generator for a variety of .NET formatters
.NET Deserialization Passive Scanner
- XSS analyzer was rewritten. Additional sanitizers were added.
- Added ReportAnalysisCompletion option to output a message that the analysis did run.
- Bugfixes and improvements.
- Better C# 7 handling
- CSRF analyzer configuration made more flexible.
Configuration file schema version has changed to 2.0, so if you had custom config settings, you’ll need to adjust to the schema and bump your file name from config-2.0.yml to config-2.1.yml or change from Version: 2.0 to Version: 2.1 if it was added to a project. - Bug fixes.
Thanks @kevin-montrose for the contributions!
Bugfix release.
- Fixes using SCS as nuget. #117
- One click code fixes were removed until a separate assembly is created. #71
The release adds support for VS2019 and bug fixes. Also:
- Adds multiple XSS sinks for WebForms (thanks Andrei!)
- Changes in SCS configuration file doesn't require to restart Visual Studio anymore.
- Warnings are suppressed for generated code.
- Adds .NET Core cookie analyzer.
This is a major release that introduces configurable taint sources, sanitizers and validators. Configuration file schema version has changed to 2.0, so if you had custom config settings, you'll need to adjust to the schema and bump your file name from config-1.0.yml
to config-2.0.yml
or change from Version: 1.0
to Version: 2.0
if it was added to a project.
With the introduction of taint sources and taint entry points warning are shown only for the tainted data. Unknowns are reported only in the Audit Mode.
Multiple improvements and fixes were done to Taint, Anti-CSRF token, XSS, SQL injection, Path traversal, XPath injection, Certificate validation analyzers.
New LDAP injection detection was added.
An issue was fixed that could surface as Session Terminated unexpectedly. Disabling 'Security Code Scan' might help prevent...
.
I would like to thank all contributors to this and previous releases. Also to everyone who has reported issues or feature requests.
Important: This release targets full .NET framework and may not run on Unix machines. Although as tested it runs fine in microsoft/dotnet 2.1 docker container on Linux, still for Unix based Continuous Integration builds it is better to use SecurityCodeScan.VS2017 NuGet package, that targets netstandard.
Added external configuration files: per user account and per project. It allows you to customize settings from built-in configuration or add your specific Sinks and Behaviors.
⚠️ Note: Configuration schema has changed in version 3.0.0 please refer to the documentation above for examples.
Audit Mode setting (Off by default) was introduced for those interested in warnings with more false positives.
Couple of issues related to VB.NET fixed:
- VB.NET projects were not analyzed when using the analyzer from NuGet.
- 'Could not load file or assembly 'Microsoft.CodeAnalysis.VisualBasic, Version=1.0.0.0...' when building C# .NET Core projects from command line with dotnet.exe
Insecure deserialization analyzers for multiple libraries and formatters:
- Json.NET
- BinaryFormatter
- FastJSON
- JavaScriptSerializer
- DataContractJsonSerializer
- NetDataContractSerializer
- XmlSerializer
- and many more...
Added warning for the usage of AllowHtml attribute.
Different input validation analyzer and CSRF analyzer improvements.
Exceptions analyzing VB.NET projects fixed.
XXE analysis expanded. More patterns to detect Open Redirect and Path Traversal. Weak hash analyzer fixes. Added request validation aspx analyzer. False positives reduced in hardcoded password manager.
Web.config analysis:
- The feature was broken. See how to enable.
- Added detection of request validation mode.
- Diagnostic messages improved.
Taint improvements:
- Area expanded.
- Taint diagnostic messages include which passed parameter is untrusted.
Various improvements were made to taint analysis. The analysis was extended from local variables into member variables. False positive fixes in:
- XSS analyzer.
- Weak hash analyzer. Added more patterns.
- Path traversal. Also added more patterns.
New features:
- Open redirect detection.