Skip to content

Commit 7f212c3

Browse files
[AAP] API10 downstream request analysis (#8232)
## Summary of changes Implement needed changes to handle API10 (downstream request) vulnerability in RASP ## Reason for change ## Implementation details ## Test coverage ## Other details <!-- Fixes #{issue} --> <!-- ⚠️ Note: Where possible, please obtain 2 approvals prior to merging. Unless CODEOWNERS specifies otherwise, for external teams it is typically best to have one review from a team member, and one review from apm-dotnet. Trivial changes do not require 2 reviews. MergeQueue is NOT enabled in this repository. If you have write access to the repo, the PR has 1-2 approvals (see above), and all of the required checks have passed, you can use the Squash and Merge button to merge the PR. If you don't have write access, or you need help, reach out in the #apm-dotnet channel in Slack. --> --------- Co-authored-by: Andrew Lock <andrew.lock@datadoghq.com> Co-authored-by: Andrew Lock <andrewlock.net@gmail.com>
1 parent 102da2d commit 7f212c3

47 files changed

Lines changed: 2443 additions & 70 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

tracer/missing-nullability-files.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,8 @@ src/Datadog.Trace/Agent/Transports/HttpStreamRequest.cs
222222
src/Datadog.Trace/Agent/Transports/HttpStreamRequestFactory.cs
223223
src/Datadog.Trace/Agent/Transports/MimeTypes.cs
224224
src/Datadog.Trace/Agent/Transports/SocketHandlerRequestFactory.cs
225+
src/Datadog.Trace/AppSec/Rasp/DownstreamSampler.cs
226+
src/Datadog.Trace/AppSec/Rasp/IDownstreamSampler.cs
225227
src/Datadog.Trace/AppSec/Waf/WafConstants.cs
226228
src/Datadog.Trace/AppSec/Waf/WafReturnCode.cs
227229
src/Datadog.Trace/ClrProfiler/Helpers/Interception.cs

tracer/src/Datadog.Trace/AppSec/AddressesConstants.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ internal static class AddressesConstants
1414
public const string FileAccess = "server.io.fs.file";
1515
public const string DBStatement = "server.db.statement";
1616
public const string DBSystem = "server.db.system";
17-
public const string UrlAccess = "server.io.net.url";
1817
public const string ShellInjection = "server.sys.shell.cmd";
1918
public const string CommandInjection = "server.sys.exec.cmd";
2019
public const string RequestMethod = "server.request.method";
@@ -32,6 +31,14 @@ internal static class AddressesConstants
3231
public const string ResponseBody = "server.response.body";
3332
public const string ResponseHeaderNoCookies = "server.response.headers.no_cookies";
3433

34+
public const string DownstreamUrl = "server.io.net.url";
35+
public const string DownstreamRequestHeaders = "server.io.net.request.headers";
36+
public const string DownstreamRequestMethod = "server.io.net.request.method";
37+
public const string DownstreamRequestBody = "server.io.net.request.body";
38+
public const string DownstreamResponseStatus = "server.io.net.response.status";
39+
public const string DownstreamResponseHeaders = "server.io.net.response.headers";
40+
public const string DownstreamResponseBody = "server.io.net.response.body";
41+
3542
public const string UserId = "usr.id";
3643
public const string UserLogin = "usr.login";
3744
public const string UserSessionId = "usr.session_id";

tracer/src/Datadog.Trace/AppSec/AppSecRequestContext.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
// <copyright file="AppSecRequestContext.cs" company="Datadog">
1+
// <copyright file="AppSecRequestContext.cs" company="Datadog">
22
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
33
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
44
// </copyright>
55

66
#nullable enable
77

8+
using System.Collections.Concurrent;
89
using System.Collections.Generic;
9-
using System.Threading;
1010
using Datadog.Trace.AppSec.Rasp;
1111
using Datadog.Trace.AppSec.Waf;
1212
using Datadog.Trace.Logging;
@@ -26,6 +26,7 @@ internal sealed partial class AppSecRequestContext
2626
private readonly object _sync = new();
2727
private readonly RaspMetricsHelper? _raspMetricsHelper = Security.Instance.RaspEnabled ? new RaspMetricsHelper() : null;
2828
private readonly List<object> _wafSecurityEvents = new();
29+
private readonly ConcurrentDictionary<ulong, bool> _sampledHttpClientRequests = new();
2930
private int _wafTimeout;
3031
private int? _wafError;
3132
private int? _wafRaspError;
@@ -140,6 +141,26 @@ internal void AddStackTrace(string stackCategory, Dictionary<string, object> sta
140141
_raspStackTraces[stackCategory].Add(stackTrace);
141142
}
142143
}
144+
145+
public bool IsHttpClientRequestSampled(ulong id)
146+
{
147+
if (_sampledHttpClientRequests.TryGetValue(id, out bool value))
148+
{
149+
return value;
150+
}
151+
152+
if (Security.Instance.SampleDownstreamRequest(this, id))
153+
{
154+
if (_sampledHttpClientRequests.Count < Security.Instance.ApiSecurityMaxDownstreamRequestBodyAnalysis)
155+
{
156+
_sampledHttpClientRequests[id] = true;
157+
return true;
158+
}
159+
}
160+
161+
_sampledHttpClientRequests[id] = false;
162+
return false;
163+
}
143164
}
144165

145166
internal partial class AppSecRequestContext

tracer/src/Datadog.Trace/AppSec/Coordinator/SecurityCoordinator.Core.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// <copyright file="SecurityCoordinator.Core.cs" company="Datadog">
1+
// <copyright file="SecurityCoordinator.Core.cs" company="Datadog">
22
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
33
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
44
// </copyright>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// <copyright file="BodyParser.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#nullable enable
7+
8+
using System;
9+
using System.Collections.Generic;
10+
using System.IO;
11+
using Datadog.Trace.Util.Json;
12+
using Datadog.Trace.Vendors.Newtonsoft.Json;
13+
14+
namespace Datadog.Trace.AppSec.Rasp;
15+
16+
internal static class BodyParser
17+
{
18+
private const int MaxElements = 1000;
19+
private const int MaxDepth = 64;
20+
private const int MaxStringSize = 1024;
21+
22+
public static object? Parse(Stream? json)
23+
{
24+
if (json is null)
25+
{
26+
return null;
27+
}
28+
29+
using var reader = new StreamReader(json);
30+
using var jsonReader = new JsonTextReader(reader) { ArrayPool = JsonArrayPool.Shared };
31+
jsonReader.MaxDepth = null; // disable built-in limit; we enforce MaxDepth ourselves
32+
33+
if (!jsonReader.Read())
34+
{
35+
return null;
36+
}
37+
38+
try
39+
{
40+
State state = new(MaxElements);
41+
return ReadValue(jsonReader, ref state, 0);
42+
}
43+
catch (JsonException)
44+
{
45+
return null;
46+
}
47+
}
48+
49+
private static object? ReadValue(JsonTextReader r, ref State state, int depth)
50+
{
51+
if (depth >= MaxDepth)
52+
{
53+
state.ObjectTooDeep = true;
54+
r.Skip();
55+
return null;
56+
}
57+
58+
if (state.ElemsLeft-- <= 0)
59+
{
60+
state.ListMapTooLarge = true;
61+
r.Skip();
62+
return null;
63+
}
64+
65+
return r.TokenType switch
66+
{
67+
JsonToken.StartObject => ReadObject(r, ref state, depth),
68+
JsonToken.StartArray => ReadArray(r, ref state, depth),
69+
JsonToken.String => ReadString(r, ref state),
70+
JsonToken.Integer => r.Value != null ? Convert.ToDouble(r.Value) : null,
71+
JsonToken.Float => r.Value != null ? Convert.ToDouble(r.Value) : null,
72+
JsonToken.Boolean => r.Value,
73+
JsonToken.Null => null,
74+
_ => null
75+
};
76+
}
77+
78+
private static string? ReadString(JsonTextReader r, ref State state)
79+
{
80+
string? val = r.Value?.ToString();
81+
if (val != null && val.Length > MaxStringSize)
82+
{
83+
state.StringTooLong = true;
84+
val = val.Substring(0, MaxStringSize);
85+
}
86+
87+
return val;
88+
}
89+
90+
private static Dictionary<string, object?> ReadObject(JsonTextReader r, ref State state, int depth)
91+
{
92+
var dict = new Dictionary<string, object?>();
93+
94+
while (r.Read() && r.TokenType != JsonToken.EndObject)
95+
{
96+
if (r.TokenType == JsonToken.PropertyName)
97+
{
98+
string propertyName = r.Value?.ToString() ?? string.Empty;
99+
100+
if (r.Read())
101+
{
102+
object? val = ReadValue(r, ref state, depth + 1);
103+
if (!state.ListMapTooLarge)
104+
{
105+
dict[propertyName] = val;
106+
}
107+
}
108+
}
109+
}
110+
111+
return dict;
112+
}
113+
114+
private static List<object?> ReadArray(JsonTextReader r, ref State state, int depth)
115+
{
116+
var list = new List<object?>();
117+
118+
while (r.Read() && r.TokenType != JsonToken.EndArray)
119+
{
120+
object? val = ReadValue(r, ref state, depth + 1);
121+
if (!state.ListMapTooLarge)
122+
{
123+
list.Add(val);
124+
}
125+
}
126+
127+
return list;
128+
}
129+
130+
public record struct State(
131+
int ElemsLeft = MaxElements,
132+
bool ObjectTooDeep = false,
133+
bool ListMapTooLarge = false,
134+
bool StringTooLong = false);
135+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// <copyright file="DownstreamSampler.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
using System.Threading;
7+
8+
namespace Datadog.Trace.AppSec.Rasp;
9+
10+
internal sealed class DownstreamSampler : IDownstreamSampler
11+
{
12+
private const long KnuthFactor = 1111111111111111111L;
13+
private readonly long _threshold;
14+
private long _globalRequestCount;
15+
16+
public DownstreamSampler(double rate)
17+
{
18+
double sanitizedRate = rate < 0.0 ? 0 : (rate > 1.0 ? 1 : rate);
19+
_threshold = SamplingCutoff(sanitizedRate);
20+
}
21+
22+
private static long SamplingCutoff(double rate)
23+
{
24+
// Maps a rate in [0.0, 1.0] to a threshold in [long.MinValue, long.MaxValue].
25+
// The hashed counter (counter * KnuthFactor) + long.MinValue is uniformly distributed
26+
// over all longs, so ~rate fraction of values will be <= the returned threshold.
27+
//
28+
// ulong.MaxValue as double rounds up to 2^64, so rate * ulong.MaxValue (as double)
29+
// can exceed long.MaxValue when rate >= 0.5. To avoid overflow when casting back to long,
30+
// subtract long.MinValue (= -2^63) in double arithmetic first, bringing the value into
31+
// [0, ~2^63), then cast to long.
32+
if (rate >= 1.0)
33+
{
34+
return long.MaxValue;
35+
}
36+
37+
// rate * 2^64 can exceed long.MaxValue for rate >= 0.5, so we subtract 2^63 first.
38+
// long.MinValue == -2^63, so: (long)(rate * 2^64 - 2^63) == (long)(rate * 2^64) + long.MinValue
39+
// Both branches are equivalent; using double subtraction first keeps us in range.
40+
return (long)((rate * (double)ulong.MaxValue) + long.MinValue);
41+
}
42+
43+
public bool SampleHttpClientRequest(AppSecRequestContext ctx, ulong requestId)
44+
{
45+
long counter = UpdateRequestCount();
46+
47+
unchecked
48+
{
49+
if ((counter * KnuthFactor) + long.MinValue > _threshold)
50+
{
51+
return false;
52+
}
53+
}
54+
55+
return true;
56+
}
57+
58+
private long UpdateRequestCount()
59+
{
60+
long initial, computed;
61+
do
62+
{
63+
initial = Interlocked.Read(ref _globalRequestCount);
64+
computed = (initial == long.MaxValue) ? 0L : initial + 1L;
65+
}
66+
while (Interlocked.CompareExchange(ref _globalRequestCount, computed, initial) != initial);
67+
68+
return computed;
69+
}
70+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// <copyright file="IDownstreamSampler.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
namespace Datadog.Trace.AppSec.Rasp;
7+
8+
internal interface IDownstreamSampler
9+
{
10+
bool SampleHttpClientRequest(AppSecRequestContext ctx, ulong requestId);
11+
}

0 commit comments

Comments
 (0)