Skip to content
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

[ASM] Send cookie values as single string to the WAF #6164

Merged
merged 6 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
#if !NETFRAMEWORK
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Datadog.Trace.AppSec.Waf;
using Datadog.Trace.Headers;
using Datadog.Trace.Util.Http;
Expand Down Expand Up @@ -45,35 +44,6 @@ private SecurityCoordinator(Security security, Span span, HttpTransport transpor

internal static SecurityCoordinator Get(Security security, Span span, HttpTransport transport) => new(security, span, transport);

public static Dictionary<string, object> ExtractHeadersFromRequest(IHeaderDictionary headers)
{
var headersDic = new Dictionary<string, object>(headers.Keys.Count);
foreach (var k in headers.Keys)
{
var currentKey = k ?? string.Empty;
if (!currentKey.Equals("cookie", System.StringComparison.OrdinalIgnoreCase))
{
currentKey = currentKey.ToLowerInvariant();
var value = GetHeaderValueForWaf(headers[currentKey]);
#if NETCOREAPP
if (!headersDic.TryAdd(currentKey, value))
{
#else
if (!headersDic.ContainsKey(currentKey))
{
headersDic.Add(currentKey, value);
}
else
{
#endif
Log.Warning("Header {Key} couldn't be added as argument to the waf", currentKey);
}
}
}

return headersDic;
}

private static object GetHeaderValueForWaf(StringValues value)
{
return (value.Count == 1 ? value[0] : value);
Expand Down Expand Up @@ -109,23 +79,7 @@ private Dictionary<string, object> GetBasicRequestArgsForWaf()
{
var request = _httpTransport.Context.Request;
var headersDic = ExtractHeadersFromRequest(request.Headers);

var cookiesDic = new Dictionary<string, List<string>>(request.Cookies.Keys.Count);
for (var i = 0; i < request.Cookies.Count; i++)
{
var cookie = request.Cookies.ElementAt(i);
var currentKey = cookie.Key ?? string.Empty;
var keyExists = cookiesDic.TryGetValue(currentKey, out var value);
if (!keyExists)
{
cookiesDic.Add(currentKey, [cookie.Value ?? string.Empty]);
}
else
{
value?.Add(cookie.Value);
}
}

var cookiesDic = ExtractCookiesFromRequest(request);
var queryStringDic = new Dictionary<string, List<string>>(request.Query.Count);
// a query string like ?test&[$slice} only fills the key part in dotnetcore and in IIS it only fills the value part, it's been decided to make it a key always
foreach (var kvp in request.Query)
Expand Down Expand Up @@ -153,7 +107,11 @@ private Dictionary<string, object> GetBasicRequestArgsForWaf()

AddAddressIfDictionaryHasElements(AddressesConstants.RequestQuery, queryStringDic);
AddAddressIfDictionaryHasElements(AddressesConstants.RequestHeaderNoCookies, headersDic);
AddAddressIfDictionaryHasElements(AddressesConstants.RequestCookies, cookiesDic);

if (cookiesDic is not null)
{
AddAddressIfDictionaryHasElements(AddressesConstants.RequestCookies, cookiesDic);
}

return addressesDictionary;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,50 +394,9 @@ public Dictionary<string, object> GetBasicRequestArgsForWaf()
{
var request = _httpTransport.Context.Request;
var headers = RequestDataHelper.GetHeaders(request);
Dictionary<string, object>? headersDic = null;
var headersDic = ExtractHeadersFromRequest(request.Headers);

if (headers is not null)
{
var headerKeys = headers.Keys;
headersDic = new Dictionary<string, object>(headerKeys.Count);
foreach (string originalKey in headerKeys)
{
var keyForDictionary = originalKey?.ToLowerInvariant() ?? string.Empty;
if (keyForDictionary != "cookie")
{
if (!headersDic.ContainsKey(keyForDictionary))
{
headersDic.Add(keyForDictionary, GetHeaderValueForWaf(headers.GetValues(originalKey)));
}
else
{
Log.Warning("Header {Key} couldn't be added as argument to the waf", keyForDictionary);
}
}
}
}

var cookies = RequestDataHelper.GetCookies(request);
Dictionary<string, List<string>>? cookiesDic = null;

if (cookies != null)
{
cookiesDic = new(cookies.AllKeys.Length);
for (var i = 0; i < cookies.Count; i++)
{
var cookie = cookies[i];
var keyForDictionary = cookie.Name ?? string.Empty;
var keyExists = cookiesDic.TryGetValue(keyForDictionary, out var value);
if (!keyExists)
{
cookiesDic.Add(keyForDictionary, new List<string> { cookie.Value ?? string.Empty });
}
else
{
value.Add(cookie.Value);
}
}
}
var cookiesDic = ExtractCookiesFromRequest(request);

var queryString = RequestDataHelper.GetQueryString(request);
Dictionary<string, string[]>? queryDic = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@
#pragma warning disable CS0282
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
using Datadog.Trace.AppSec.Waf;
using Datadog.Trace.AppSec.Waf.ReturnTypes.Managed;
using Datadog.Trace.ExtensionMethods;
using Datadog.Trace.Logging;
using Datadog.Trace.Telemetry;
using Datadog.Trace.Telemetry.Metrics;
using Datadog.Trace.Vendors.MessagePack;
using Datadog.Trace.Vendors.Newtonsoft.Json;
using Datadog.Trace.Util;
using Datadog.Trace.Vendors.Serilog.Events;
#if !NETFRAMEWORK
using System.Linq;
using Microsoft.AspNetCore.Http;
#else
using System.Collections.Specialized;
using System.Web;
#endif

namespace Datadog.Trace.AppSec.Coordinator;

Expand Down Expand Up @@ -165,5 +167,84 @@ public void AddResponseHeadersToSpanAndCleanup()
_httpTransport.DisposeAdditiveContext();
}

internal static Dictionary<string, object> ExtractCookiesFromRequest(HttpRequest request)
{
var cookies = RequestDataHelper.GetCookies(request);
var cookiesDic = new Dictionary<string, object>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could instantiate it inside clause cookies != null to avoid an allocation in some cases and have a Dictionary<string, object>? as return type, especially that I see you check for null when the method is called

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Thanks!


if (cookies != null)
{
for (var i = 0; i < cookies.Count; i++)
{
#if NETCOREAPP || NETSTANDARD
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we could use both SecurityCoordinator.Core and SecurityCoordinator.Framework to avoid these ifs?

var cookie = cookies.ElementAt(i);
var keyForDictionary = cookie.Key ?? string.Empty;
#else
var cookie = cookies[i];
var keyForDictionary = cookie.Name ?? string.Empty;
#endif
if (!cookiesDic.TryGetValue(keyForDictionary, out var value))
{
cookiesDic.Add(keyForDictionary, cookie.Value ?? string.Empty);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it was like this before, but wondering if we really want to send cookies with a null key and/or null value to the waf 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that we should exclude cookies with null keys or values. I have updated the code.

}
else
{
if (value is string stringValue)
{
cookiesDic[keyForDictionary] = new List<string> { stringValue, cookie.Value ?? string.Empty };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, does it make sense to send an empty value to the list, especially if there are other actual values in the list already?

}
else if (value is List<string> valueList)
{
valueList.Add(cookie.Value ?? string.Empty);
}
else
{
Log.Warning("Cookie {Key} couldn't be added as argument to the waf", keyForDictionary);
}
}
}
}

return cookiesDic;
}

#if NETFRAMEWORK
internal static Dictionary<string, object> ExtractHeadersFromRequest(NameValueCollection headers)
anna-git marked this conversation as resolved.
Show resolved Hide resolved
#else
internal static Dictionary<string, object> ExtractHeadersFromRequest(IHeaderDictionary headers)
#endif
{
var headersDic = new Dictionary<string, object>(headers.Keys.Count);
foreach (string key in headers.Keys)
{
var currentKey = key ?? string.Empty;
if (!currentKey.Equals("cookie", System.StringComparison.OrdinalIgnoreCase))
{
currentKey = currentKey.ToLowerInvariant();

#if NETCOREAPP || NETSTANDARD
var value = GetHeaderValueForWaf(headers[currentKey]);
#else
var value = GetHeaderValueForWaf(headers.GetValues(currentKey));
#endif
#if NETCOREAPP
if (!headersDic.TryAdd(currentKey, value))
{
#else
if (!headersDic.ContainsKey(currentKey))
{
headersDic.Add(currentKey, value);
}
else
{
#endif
Log.Warning("Header {Key} couldn't be added as argument to the waf", currentKey);
}
}
}

return headersDic;
}

private static Span TryGetRoot(Span span) => span.Context.TraceContext?.RootSpan ?? span;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#pragma warning disable SA1402 // File may only contain a single class
#pragma warning disable SA1649 // File name must match first type name

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
Expand Down Expand Up @@ -46,6 +47,7 @@ public AspNetCore2Rasp(AspNetCoreTestFixture fixture, ITestOutputHelper outputHe
EnableRasp();
SetSecurity(true);
EnableIast(enableIast);
AddCookies(new Dictionary<string, string> { { "cookie-key", "cookie-value" } });
SetEnvironmentVariable(ConfigurationKeys.Iast.IsIastDeduplicationEnabled, "false");
SetEnvironmentVariable(ConfigurationKeys.Iast.VulnerabilitiesPerRequest, "100");
SetEnvironmentVariable(ConfigurationKeys.Iast.RequestSampling, "100");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#pragma warning disable SA1649 // File name must match first type name

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
Expand Down Expand Up @@ -70,6 +71,7 @@ public AspNetCore5Rasp(AspNetCoreTestFixture fixture, ITestOutputHelper outputHe
EnableRasp();
SetSecurity(true);
EnableIast(enableIast);
AddCookies(new Dictionary<string, string> { { "cookie-key", "cookie-value" } });
SetEnvironmentVariable(ConfigurationKeys.Iast.IsIastDeduplicationEnabled, "false");
SetEnvironmentVariable(ConfigurationKeys.Iast.VulnerabilitiesPerRequest, "100");
SetEnvironmentVariable(ConfigurationKeys.Iast.RequestSampling, "100");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
#pragma warning disable SA1649 // File name must match first type name

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Datadog.Trace.AppSec;
using Datadog.Trace.Iast.Telemetry;
using Datadog.Trace.Security.IntegrationTests.IAST;
using Datadog.Trace.TestHelpers;
Expand Down Expand Up @@ -68,6 +68,7 @@ public AspNetMvc5RaspTests(IisFixture iisFixture, ITestOutputHelper output, bool
EnableRasp();
SetSecurity(true);
EnableIast(enableIast);
AddCookies(new Dictionary<string, string> { { "cookie-key", "cookie-value" } });
EnableIastTelemetry((int)IastMetricsVerbosityLevel.Off);
EnableEvidenceRedaction(false);
SetEnvironmentVariable("DD_IAST_DEDUPLICATION_ENABLED", "false");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
_dd.appsec.fp.http.endpoint: http-get-92238171-0a2bbc6e-,
_dd.appsec.fp.http.header: hdr-0100000001-3626b5f8-3-bf93958a,
_dd.appsec.fp.http.network: net-1-1000000000,
_dd.appsec.fp.session: ssn--bd9bce81-d0fff5a7-,
_dd.appsec.json: {"triggers":[{"rule":{"id":"rasp-932-100","name":"Shell injection exploit","tags":{"category":"vulnerability_trigger","type":"command_injection"}},"rule_matches":[{"operator":"shi_detector","operator_value":"","parameters":[{"address":null,"highlight":[";evilCommand"],"key_path":null,"value":null}]}],"span_id": XXX}]},
_dd.origin: appsec,
_dd.runtime_family: dotnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
_dd.appsec.fp.http.endpoint: http-get-e1e32f93-3b9c358f-,
_dd.appsec.fp.http.header: hdr-0100000001-3626b5f8-3-bf93958a,
_dd.appsec.fp.http.network: net-1-1000000000,
_dd.appsec.fp.session: ssn--bd9bce81-d0fff5a7-,
_dd.appsec.json: {"triggers":[{"rule":{"id":"rasp-001-001","name":"Path traversal attack","tags":{"category":"vulnerability_trigger","type":"lfi"}},"rule_matches":[{"operator":"lfi_detector","operator_value":"","parameters":[{"address":null,"highlight":["/etc/password"],"key_path":null,"value":null}]}],"span_id": XXX}]},
_dd.origin: appsec,
_dd.runtime_family: dotnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
_dd.appsec.fp.http.endpoint: http-get-05b4d989-4740ae63-,
_dd.appsec.fp.http.header: hdr-0100000001-3626b5f8-3-bf93958a,
_dd.appsec.fp.http.network: net-1-1000000000,
_dd.appsec.fp.session: ssn--bd9bce81-d0fff5a7-,
_dd.appsec.json: {"triggers":[{"rule":{"id":"rasp-002-001","name":"Server-side request forgery","tags":{"category":"vulnerability_trigger","type":"ssrf"}},"rule_matches":[{"operator":"ssrf_detector","operator_value":"","parameters":[{"address":null,"highlight":["127.0.0.1"],"key_path":null,"value":null}]}],"span_id": XXX}]},
_dd.origin: appsec,
_dd.runtime_family: dotnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
span.kind: server,
_dd.appsec.fp.http.header: hdr-0100000100-3626b5f8-2-da57b738,
_dd.appsec.fp.http.network: net-1-1000000000,
_dd.appsec.fp.session: ssn--bd9bce81-d0fff5a7-,
_dd.appsec.json: {"triggers":[{"rule":{"id":"rasp-942-100","name":"SQL injection exploit","tags":{"category":"vulnerability_trigger","type":"sql_injection"}},"rule_matches":[{"operator":"sqli_detector","operator_value":"","parameters":[{"address":null,"highlight":["' or '1'='1"],"key_path":null,"value":null}]}],"span_id": XXX}]},
_dd.origin: appsec,
_dd.runtime_family: dotnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
_dd.appsec.fp.http.endpoint: http-get-92238171-0a2bbc6e-,
_dd.appsec.fp.http.header: hdr-0000000001-3626b5f8-3-bf93958a,
_dd.appsec.fp.http.network: net-1-1000000000,
_dd.appsec.fp.session: ssn--bd9bce81-d0fff5a7-,
_dd.appsec.json: {"triggers":[{"rule":{"id":"rasp-932-100","name":"Shell injection exploit","tags":{"category":"vulnerability_trigger","type":"command_injection"}},"rule_matches":[{"operator":"shi_detector","operator_value":"","parameters":[{"address":null,"highlight":[";evilCommand"],"key_path":null,"value":null}]}],"span_id": XXX}]},
_dd.origin: appsec,
_dd.runtime_family: dotnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
_dd.appsec.fp.http.endpoint: http-get-e1e32f93-3b9c358f-,
_dd.appsec.fp.http.header: hdr-0000000001-3626b5f8-3-bf93958a,
_dd.appsec.fp.http.network: net-1-1000000000,
_dd.appsec.fp.session: ssn--bd9bce81-d0fff5a7-,
_dd.appsec.json: {"triggers":[{"rule":{"id":"rasp-001-001","name":"Path traversal attack","tags":{"category":"vulnerability_trigger","type":"lfi"}},"rule_matches":[{"operator":"lfi_detector","operator_value":"","parameters":[{"address":null,"highlight":["/etc/password"],"key_path":null,"value":null}]}],"span_id": XXX}]},
_dd.origin: appsec,
_dd.runtime_family: dotnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
_dd.appsec.fp.http.endpoint: http-get-05b4d989-4740ae63-,
_dd.appsec.fp.http.header: hdr-0000000001-3626b5f8-3-bf93958a,
_dd.appsec.fp.http.network: net-1-1000000000,
_dd.appsec.fp.session: ssn--bd9bce81-d0fff5a7-,
_dd.appsec.json: {"triggers":[{"rule":{"id":"rasp-002-001","name":"Server-side request forgery","tags":{"category":"vulnerability_trigger","type":"ssrf"}},"rule_matches":[{"operator":"ssrf_detector","operator_value":"","parameters":[{"address":null,"highlight":["127.0.0.1"],"key_path":null,"value":null}]}],"span_id": XXX}]},
_dd.origin: appsec,
_dd.runtime_family: dotnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
span.kind: server,
_dd.appsec.fp.http.header: hdr-0000000100-3626b5f8-2-da57b738,
_dd.appsec.fp.http.network: net-1-1000000000,
_dd.appsec.fp.session: ssn--bd9bce81-d0fff5a7-,
_dd.appsec.json: {"triggers":[{"rule":{"id":"rasp-942-100","name":"SQL injection exploit","tags":{"category":"vulnerability_trigger","type":"sql_injection"}},"rule_matches":[{"operator":"sqli_detector","operator_value":"","parameters":[{"address":null,"highlight":["' or '1'='1"],"key_path":null,"value":null}]}],"span_id": XXX}]},
_dd.origin: appsec,
_dd.runtime_family: dotnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
_dd.appsec.fp.http.endpoint: http-get-92238171-0a2bbc6e-,
_dd.appsec.fp.http.header: hdr-0000000001-3626b5f8-3-bf93958a,
_dd.appsec.fp.http.network: net-1-1000000000,
_dd.appsec.fp.session: ssn----,
_dd.appsec.fp.session: ssn--bd9bce81-d0fff5a7-,
_dd.appsec.json: {"triggers":[{"rule":{"id":"rasp-932-100","name":"Shell injection exploit","tags":{"category":"vulnerability_trigger","type":"command_injection"}},"rule_matches":[{"operator":"shi_detector","operator_value":"","parameters":[{"address":null,"highlight":[";evilCommand"],"key_path":null,"value":null}]}],"span_id": XXX}]},
_dd.origin: appsec,
_dd.runtime_family: dotnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
_dd.appsec.fp.http.endpoint: http-get-e1e32f93-3b9c358f-,
_dd.appsec.fp.http.header: hdr-0000000001-3626b5f8-3-bf93958a,
_dd.appsec.fp.http.network: net-1-1000000000,
_dd.appsec.fp.session: ssn----,
_dd.appsec.fp.session: ssn--bd9bce81-d0fff5a7-,
_dd.appsec.json: {"triggers":[{"rule":{"id":"rasp-001-001","name":"Path traversal attack","tags":{"category":"vulnerability_trigger","type":"lfi"}},"rule_matches":[{"operator":"lfi_detector","operator_value":"","parameters":[{"address":null,"highlight":["/etc/password"],"key_path":null,"value":null}]}],"span_id": XXX}]},
_dd.origin: appsec,
_dd.runtime_family: dotnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
_dd.appsec.fp.http.endpoint: http-get-05b4d989-4740ae63-,
_dd.appsec.fp.http.header: hdr-0000000001-3626b5f8-3-bf93958a,
_dd.appsec.fp.http.network: net-1-1000000000,
_dd.appsec.fp.session: ssn----,
_dd.appsec.fp.session: ssn--bd9bce81-d0fff5a7-,
_dd.appsec.json: {"triggers":[{"rule":{"id":"rasp-002-001","name":"Server-side request forgery","tags":{"category":"vulnerability_trigger","type":"ssrf"}},"rule_matches":[{"operator":"ssrf_detector","operator_value":"","parameters":[{"address":null,"highlight":["127.0.0.1"],"key_path":null,"value":null}]}],"span_id": XXX}]},
_dd.origin: appsec,
_dd.runtime_family: dotnet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
_dd.appsec.fp.http.endpoint: http-get-ece9044c-4740ae63-,
_dd.appsec.fp.http.header: hdr-0000000001-3626b5f8-3-bf93958a,
_dd.appsec.fp.http.network: net-1-1000000000,
_dd.appsec.fp.session: ssn----,
_dd.appsec.fp.session: ssn--bd9bce81-d0fff5a7-,
_dd.appsec.json: {"triggers":[{"rule":{"id":"rasp-002-001","name":"Server-side request forgery","tags":{"category":"vulnerability_trigger","type":"ssrf"}},"rule_matches":[{"operator":"ssrf_detector","operator_value":"","parameters":[{"address":null,"highlight":["127.0.0.1"],"key_path":null,"value":null}]}],"span_id": XXX}]},
_dd.origin: appsec,
_dd.runtime_family: dotnet
Expand Down
Loading
Loading