Skip to content

Commit f1d1463

Browse files
authored
fix: Fix Code Scanning Alerts (#241)
* fix: Fix Code Scanning Alerts
1 parent 96d32e0 commit f1d1463

File tree

4 files changed

+191
-17
lines changed

4 files changed

+191
-17
lines changed

src/AzureEventGridSimulator.Tests/UnitTests/Middleware/SasKeyValidatorAegSasTokenTests.cs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,134 @@ public void GivenTokenWithInvalidExpirationFormat_WhenValidated_ThenReturnsFalse
112112

113113
result.ShouldBeFalse();
114114
}
115+
116+
[Fact]
117+
public void GivenTokenWithSignatureContainingNewline_WhenValidated_ThenSanitizesNewlineInLog()
118+
{
119+
// Create a token with an invalid signature containing a newline character
120+
const string maliciousSignature = "fake\nsignature";
121+
var token =
122+
$"r=http%3A%2F%2Flocalhost&e={DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds()}&s={maliciousSignature}";
123+
var headers = new HeaderDictionary { { Constants.AegSasTokenHeader, token } };
124+
125+
Validator.IsValid(headers, ValidTopicKey);
126+
127+
// Verify that the logger was called with the sanitized signature (\\n instead of \n)
128+
Logger
129+
.Received()
130+
.Log(
131+
LogLevel.Warning,
132+
Arg.Any<EventId>(),
133+
Arg.Is<object>(o =>
134+
(o.ToString() ?? "").Contains("fake\\nsignature")
135+
&& !(o.ToString() ?? "").Contains("fake\nsignature")
136+
),
137+
Arg.Any<Exception?>(),
138+
Arg.Any<Func<object, Exception?, string>>()
139+
);
140+
}
141+
142+
[Fact]
143+
public void GivenTokenWithSignatureContainingCarriageReturn_WhenValidated_ThenSanitizesCarriageReturnInLog()
144+
{
145+
// Create a token with an invalid signature containing a carriage return character
146+
const string maliciousSignature = "fake\rsignature";
147+
var token =
148+
$"r=http%3A%2F%2Flocalhost&e={DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds()}&s={maliciousSignature}";
149+
var headers = new HeaderDictionary { { Constants.AegSasTokenHeader, token } };
150+
151+
Validator.IsValid(headers, ValidTopicKey);
152+
153+
// Verify that the logger was called with the sanitized signature (\\r instead of \r)
154+
Logger
155+
.Received()
156+
.Log(
157+
LogLevel.Warning,
158+
Arg.Any<EventId>(),
159+
Arg.Is<object>(o =>
160+
(o.ToString() ?? "").Contains("fake\\rsignature")
161+
&& !(o.ToString() ?? "").Contains("fake\rsignature")
162+
),
163+
Arg.Any<Exception?>(),
164+
Arg.Any<Func<object, Exception?, string>>()
165+
);
166+
}
167+
168+
[Fact]
169+
public void GivenTokenWithSignatureContainingTab_WhenValidated_ThenSanitizesTabInLog()
170+
{
171+
// Create a token with an invalid signature containing a tab character
172+
const string maliciousSignature = "fake\tsignature";
173+
var token =
174+
$"r=http%3A%2F%2Flocalhost&e={DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds()}&s={maliciousSignature}";
175+
var headers = new HeaderDictionary { { Constants.AegSasTokenHeader, token } };
176+
177+
Validator.IsValid(headers, ValidTopicKey);
178+
179+
// Verify that the logger was called with the sanitized signature (\\t instead of \t)
180+
Logger
181+
.Received()
182+
.Log(
183+
LogLevel.Warning,
184+
Arg.Any<EventId>(),
185+
Arg.Is<object>(o =>
186+
(o.ToString() ?? "").Contains("fake\\tsignature")
187+
&& !(o.ToString() ?? "").Contains("fake\tsignature")
188+
),
189+
Arg.Any<Exception?>(),
190+
Arg.Any<Func<object, Exception?, string>>()
191+
);
192+
}
193+
194+
[Fact]
195+
public void GivenTokenWithSignatureContainingMultipleControlCharacters_WhenValidated_ThenSanitizesAllControlCharactersInLog()
196+
{
197+
// Create a token with an invalid signature containing multiple control characters
198+
const string maliciousSignature = "fake\n\r\t\0signature";
199+
var token =
200+
$"r=http%3A%2F%2Flocalhost&e={DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds()}&s={maliciousSignature}";
201+
var headers = new HeaderDictionary { { Constants.AegSasTokenHeader, token } };
202+
203+
Validator.IsValid(headers, ValidTopicKey);
204+
205+
// Verify that all control characters are sanitized
206+
Logger
207+
.Received()
208+
.Log(
209+
LogLevel.Warning,
210+
Arg.Any<EventId>(),
211+
Arg.Is<object>(o =>
212+
(o.ToString() ?? "").Contains("fake\\n\\r\\t\\0signature")
213+
&& !(o.ToString() ?? "").Contains("fake\n\r\t\0signature")
214+
),
215+
Arg.Any<Exception?>(),
216+
Arg.Any<Func<object, Exception?, string>>()
217+
);
218+
}
219+
220+
[Fact]
221+
public void GivenTokenWithSignatureContainingDelCharacter_WhenValidated_ThenSanitizesDelInLog()
222+
{
223+
// Create a token with an invalid signature containing the DEL character (ASCII 127)
224+
var maliciousSignature = $"fake{(char)127}signature";
225+
var token =
226+
$"r=http%3A%2F%2Flocalhost&e={DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds()}&s={maliciousSignature}";
227+
var headers = new HeaderDictionary { { Constants.AegSasTokenHeader, token } };
228+
229+
Validator.IsValid(headers, ValidTopicKey);
230+
231+
// Verify that the DEL character is sanitized as \x7F
232+
Logger
233+
.Received()
234+
.Log(
235+
LogLevel.Warning,
236+
Arg.Any<EventId>(),
237+
Arg.Is<object>(o =>
238+
(o.ToString() ?? "").Contains("fake\\x7Fsignature")
239+
&& !(o.ToString() ?? "").Contains($"fake{(char)127}signature")
240+
),
241+
Arg.Any<Exception?>(),
242+
Arg.Any<Func<object, Exception?, string>>()
243+
);
244+
}
115245
}

src/AzureEventGridSimulator/Domain/Services/Dashboard/EventHistoryStore.cs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -156,22 +156,26 @@ public DashboardStats GetStats(int topicsActive)
156156
var totalPending = 0;
157157

158158
foreach (var record in records)
159-
foreach (var delivery in record.GetDeliveries())
160-
switch (delivery.Status)
159+
{
160+
foreach (var delivery in record.GetDeliveries())
161161
{
162-
case DeliveryStatus.Delivered:
163-
totalDelivered++;
164-
break;
165-
case DeliveryStatus.Failed:
166-
case DeliveryStatus.DeadLettered:
167-
totalFailed++;
168-
break;
169-
case DeliveryStatus.Pending:
170-
case DeliveryStatus.InProgress:
171-
case DeliveryStatus.Retrying:
172-
totalPending++;
173-
break;
162+
switch (delivery.Status)
163+
{
164+
case DeliveryStatus.Delivered:
165+
totalDelivered++;
166+
break;
167+
case DeliveryStatus.Failed:
168+
case DeliveryStatus.DeadLettered:
169+
totalFailed++;
170+
break;
171+
case DeliveryStatus.Pending:
172+
case DeliveryStatus.InProgress:
173+
case DeliveryStatus.Retrying:
174+
totalPending++;
175+
break;
176+
}
174177
}
178+
}
175179

176180
return new DashboardStats(
177181
_totalEventsReceived,

src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ private async Task HandleNotificationRequest(
9595
ILogger logger
9696
)
9797
{
98-
var contentType = context.Request.Headers.ContentType.FirstOrDefault();
99-
10098
// 1. Validate the SAS key/token if configured
10199
if (!string.IsNullOrWhiteSpace(topic.Key))
102100
{
@@ -152,6 +150,7 @@ await context.WriteErrorResponse(
152150
: validationResult2.ErrorMessage + context.GenerateReportSuffix();
153151

154152
// Record the rejection in event history
153+
var contentType = context.Request.Headers.ContentType.FirstOrDefault();
155154
RecordRejection(
156155
eventHistoryService,
157156
topic.Name,

src/AzureEventGridSimulator/Infrastructure/Middleware/SasKeyValidator.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,12 @@ private SasValidationResult ValidateToken(string token, string key)
206206
if (string.Equals(signature, computedSignature, StringComparison.Ordinal))
207207
return new SasValidationResult(true);
208208

209+
// Sanitize signature to prevent log forging by escaping all control characters
210+
var sanitizedSignature = SanitizeForLogging(signature);
209211
logger.LogWarning(
210212
"SAS token signature mismatch. Expected: {Expected}, Got: {Actual}",
211213
computedSignature,
212-
signature
214+
sanitizedSignature
213215
);
214216

215217
return new SasValidationResult(false, SasValidationFailureReason.SignatureMismatch);
@@ -219,4 +221,43 @@ private SasValidationResult ValidateToken(string token, string key)
219221
return new SasValidationResult(false, SasValidationFailureReason.InvalidBase64);
220222
}
221223
}
224+
225+
/// <summary>
226+
/// Sanitizes a string for logging by escaping all control characters (code points &lt; 32 and DEL at 127)
227+
/// to prevent log forging attacks.
228+
/// </summary>
229+
/// <param name="input">The string to sanitize</param>
230+
/// <returns>A sanitized string with control characters escaped</returns>
231+
private static string SanitizeForLogging(string input)
232+
{
233+
// Use input.Length * 2 capacity to reduce reallocations when control characters expand
234+
// (e.g., '\n' becomes "\\n" which is 2 characters instead of 1)
235+
var sb = new StringBuilder(input.Length * 2);
236+
foreach (var c in input)
237+
{
238+
if (c < 32 || c == 127) // Control characters: code points < 32 and DEL (127)
239+
{
240+
sb.Append(
241+
c switch
242+
{
243+
'\n' => "\\n",
244+
'\r' => "\\r",
245+
'\t' => "\\t",
246+
'\f' => "\\f",
247+
'\b' => "\\b",
248+
'\0' => "\\0",
249+
'\a' => "\\a",
250+
'\v' => "\\v",
251+
(char)127 => "\\x7F", // DEL character
252+
_ => $"\\x{((int)c):X2}" // Escape other control chars as hex
253+
}
254+
);
255+
}
256+
else
257+
{
258+
sb.Append(c);
259+
}
260+
}
261+
return sb.ToString();
262+
}
222263
}

0 commit comments

Comments
 (0)