-
Notifications
You must be signed in to change notification settings - Fork 305
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Optimize request validator but keep backwards compatible
1 parent
25bdab1
commit 813a64b
Showing
6 changed files
with
385 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -235,3 +235,4 @@ html/ | |
|
||
# sonar cloud stuff | ||
.sonarqube | ||
/test/Twilio.Benchmark/BenchmarkDotNet.Artifacts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
using System.Collections.Specialized; | ||
using BenchmarkDotNet.Attributes; | ||
using BenchmarkDotNet.Running; | ||
using Twilio.Security; | ||
|
||
var summary = BenchmarkRunner.Run<RequestValidationBenchmark>(); | ||
Console.Write(summary); | ||
|
||
[MemoryDiagnoser] | ||
public class RequestValidationBenchmark | ||
{ | ||
private const string Secret = "12345"; | ||
private const string UnhappyPathUrl = "HTTP://MyCompany.com:8080/myapp.php?foo=1&bar=2"; | ||
private const string UnhappyPathSignature = "eYYN9fMlxrQMXOsr7bIzoPTrbxA="; | ||
private const string HappyPathUrl = "https://mycompany.com/myapp.php?foo=1&bar=2"; | ||
private const string HappyPathSignature = "3LL3BFKOcn80artVM5inMPFpmtU="; | ||
private static readonly NameValueCollection UnhappyPathParameters = new() | ||
{ | ||
{"ToCountry", "US"}, | ||
{"ToState", "OH"}, | ||
{"SmsMessageSid", "SMcea2a3bd6f50296f8fab60f377db03eb"}, | ||
{"NumMedia", "0"}, | ||
{"ToCity", "UTICA"}, | ||
{"FromZip", "20705"}, | ||
{"SmsSid", "SMcea2a3bd6f50296f8fab60f377db03eb"}, | ||
{"FromState", "DC"}, | ||
{"SmsStatus", "received"}, | ||
{"FromCity", "BELTSVILLE"}, | ||
{"Body", "Ahoy!"}, | ||
{"FromCountry", "US"}, | ||
{"To", "+10123456789"}, | ||
{"ToZip", "43037"}, | ||
{"NumSegments", "1"}, | ||
{"ReferralNumMedia", "0"}, | ||
{"MessageSid", "SMcea2a3bd6f50296f8fab60f377db03eb"}, | ||
{"AccountSid", "ACe718619887aac3ee5b21edafbvsdf6h7fgb"}, | ||
{"From", "+10123456789"}, | ||
{"ApiVersion", "2010-04-01"} | ||
}; | ||
private static readonly Dictionary<string, string> HappyPathParameters = new() | ||
{ | ||
{"ToCountry", "US"}, | ||
{"ToState", "OH"}, | ||
{"SmsMessageSid", "SMcea2a3bd6f50296f8fab60f377db03eb"}, | ||
{"NumMedia", "0"}, | ||
{"ToCity", "UTICA"}, | ||
{"FromZip", "20705"}, | ||
{"SmsSid", "SMcea2a3bd6f50296f8fab60f377db03eb"}, | ||
{"FromState", "DC"}, | ||
{"SmsStatus", "received"}, | ||
{"FromCity", "BELTSVILLE"}, | ||
{"Body", "Ahoy!"}, | ||
{"FromCountry", "US"}, | ||
{"To", "+10123456789"}, | ||
{"ToZip", "43037"}, | ||
{"NumSegments", "1"}, | ||
{"ReferralNumMedia", "0"}, | ||
{"MessageSid", "SMcea2a3bd6f50296f8fab60f377db03eb"}, | ||
{"AccountSid", "ACe718619887aac3ee5b21edafbvsdf6h7fgb"}, | ||
{"From", "+10123456789"}, | ||
{"ApiVersion", "2010-04-01"} | ||
}; | ||
|
||
|
||
[Benchmark] | ||
public void OriginalUnhappyPath() | ||
{ | ||
var requestValidator = new RequestValidatorOriginal(Secret); | ||
requestValidator.Validate(UnhappyPathUrl, UnhappyPathParameters, UnhappyPathSignature); | ||
} | ||
|
||
[Benchmark] | ||
public void CurrentUnhappyPath() | ||
{ | ||
var requestValidator = new RequestValidator(Secret); | ||
requestValidator.Validate(UnhappyPathUrl, UnhappyPathParameters, UnhappyPathSignature); | ||
} | ||
|
||
[Benchmark] | ||
public void OriginalHappyPath() | ||
{ | ||
var requestValidator = new RequestValidatorOriginal(Secret); | ||
requestValidator.Validate(HappyPathUrl, HappyPathParameters, HappyPathSignature); | ||
} | ||
|
||
[Benchmark] | ||
public void CurrentHappyPath() | ||
{ | ||
var requestValidator = new RequestValidator(Secret); | ||
requestValidator.Validate(HappyPathUrl, HappyPathParameters, HappyPathSignature); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Specialized; | ||
using System.Security.Cryptography; | ||
using System.Text; | ||
|
||
namespace Twilio.Security | ||
{ | ||
/// <summary> | ||
/// Twilio request validator | ||
/// </summary> | ||
public class RequestValidatorOriginal | ||
{ | ||
private readonly HMACSHA1 _hmac; | ||
private readonly SHA256 _sha; | ||
|
||
/// <summary> | ||
/// Create a new RequestValidator | ||
/// </summary> | ||
/// <param name="secret">Signing secret</param> | ||
public RequestValidatorOriginal(string secret) | ||
{ | ||
_hmac = new HMACSHA1(Encoding.UTF8.GetBytes(secret)); | ||
_sha = SHA256.Create(); | ||
} | ||
|
||
/// <summary> | ||
/// Validate against a request | ||
/// </summary> | ||
/// <param name="url">Request URL</param> | ||
/// <param name="parameters">Request parameters</param> | ||
/// <param name="expected">Expected result</param> | ||
/// <returns>true if the signature matches the result; false otherwise</returns> | ||
public bool Validate(string url, NameValueCollection parameters, string expected) | ||
{ | ||
return Validate(url, ToDictionary(parameters), expected); | ||
} | ||
|
||
/// <summary> | ||
/// Validate against a request | ||
/// </summary> | ||
/// <param name="url">Request URL</param> | ||
/// <param name="parameters">Request parameters</param> | ||
/// <param name="expected">Expected result</param> | ||
/// <returns>true if the signature matches the result; false otherwise</returns> | ||
public bool Validate(string url, IDictionary<string, string> parameters, string expected) | ||
{ | ||
// check signature of url with and without port, since sig generation on back end is inconsistent | ||
var signatureWithoutPort = GetValidationSignature(RemovePort(url), parameters); | ||
var signatureWithPort = GetValidationSignature(AddPort(url), parameters); | ||
// If either url produces a valid signature, we accept the request as valid | ||
return SecureCompare(signatureWithoutPort, expected) || SecureCompare(signatureWithPort, expected); | ||
} | ||
|
||
public bool Validate(string url, string body, string expected) | ||
{ | ||
var paramString = new UriBuilder(url).Query.TrimStart('?'); | ||
var bodyHash = ""; | ||
foreach (var param in paramString.Split('&')) | ||
{ | ||
var split = param.Split('='); | ||
if (split[0] == "bodySHA256") | ||
{ | ||
bodyHash = Uri.UnescapeDataString(split[1]); | ||
} | ||
} | ||
|
||
return Validate(url, new Dictionary<string, string>(), expected) && ValidateBody(body, bodyHash); | ||
} | ||
|
||
public bool ValidateBody(string rawBody, string expected) | ||
{ | ||
var signature = _sha.ComputeHash(Encoding.UTF8.GetBytes(rawBody)); | ||
return SecureCompare(BitConverter.ToString(signature).Replace("-", "").ToLower(), expected); | ||
} | ||
|
||
private static IDictionary<string, string> ToDictionary(NameValueCollection col) | ||
{ | ||
var dict = new Dictionary<string, string>(); | ||
foreach (var k in col.AllKeys) | ||
{ | ||
dict.Add(k, col[k]); | ||
} | ||
return dict; | ||
} | ||
|
||
private string GetValidationSignature(string url, IDictionary<string, string> parameters) | ||
{ | ||
var b = new StringBuilder(url); | ||
if (parameters != null) | ||
{ | ||
var sortedKeys = new List<string>(parameters.Keys); | ||
sortedKeys.Sort(StringComparer.Ordinal); | ||
|
||
foreach (var key in sortedKeys) | ||
{ | ||
b.Append(key).Append(parameters[key] ?? ""); | ||
} | ||
} | ||
|
||
var hash = _hmac.ComputeHash(Encoding.UTF8.GetBytes(b.ToString())); | ||
return Convert.ToBase64String(hash); | ||
} | ||
|
||
private static bool SecureCompare(string a, string b) | ||
{ | ||
if (a == null || b == null) | ||
{ | ||
return false; | ||
} | ||
|
||
var n = a.Length; | ||
if (n != b.Length) | ||
{ | ||
return false; | ||
} | ||
|
||
var mismatch = 0; | ||
for (var i = 0; i < n; i++) | ||
{ | ||
mismatch |= a[i] ^ b[i]; | ||
} | ||
|
||
return mismatch == 0; | ||
} | ||
|
||
private string RemovePort(string url) | ||
{ | ||
return SetPort(url, -1); | ||
} | ||
|
||
private string AddPort(string url) | ||
{ | ||
var uri = new UriBuilder(url); | ||
return SetPort(url, uri.Port); | ||
} | ||
|
||
private string SetPort(string url, int port) | ||
{ | ||
var uri = new UriBuilder(url); | ||
uri.Host = PreserveCase(url, uri.Host); | ||
if (port == -1) | ||
{ | ||
uri.Port = port; | ||
} | ||
else if ((port != 443) && (port != 80)) | ||
{ | ||
uri.Port = port; | ||
} | ||
else | ||
{ | ||
uri.Port = uri.Scheme == "https" ? 443 : 80; | ||
} | ||
var scheme = PreserveCase(url, uri.Scheme); | ||
return uri.Uri.OriginalString.Replace(uri.Scheme, scheme); | ||
} | ||
|
||
private string PreserveCase(string url, string replacementString) | ||
{ | ||
var startIndex = url.IndexOf(replacementString, StringComparison.OrdinalIgnoreCase); | ||
return url.Substring(startIndex, replacementString.Length); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net7.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>disable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Compile Include="..\..\src\Twilio\Security\RequestValidator.cs" Link="RequestValidator.cs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="BenchmarkDotNet" Version="0.13.3" /> | ||
</ItemGroup> | ||
</Project> |