Skip to content

Commit 6bf72b6

Browse files
Copilotbaywet
andauthored
fix(library): avoid components key validation timeouts under load
Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
1 parent 1355f08 commit 6bf72b6

2 files changed

Lines changed: 109 additions & 7 deletions

File tree

‎src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs‎

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4-
using System;
54
using System.Collections.Generic;
65
using System.Text.RegularExpressions;
76

@@ -11,12 +10,17 @@ namespace Microsoft.OpenApi
1110
/// The validation rules for <see cref="OpenApiComponents"/>.
1211
/// </summary>
1312
[OpenApiRule]
14-
public static class OpenApiComponentsRules
13+
public static partial class OpenApiComponentsRules
1514
{
1615
/// <summary>
17-
/// The key regex.
16+
/// The key regex pattern.
1817
/// </summary>
19-
internal static readonly Regex KeyRegex = new(@"^[a-zA-Z0-9\.\-_]+$", RegexOptions.None, TimeSpan.FromMilliseconds(100));
18+
internal const string KeyPattern = @"^[a-zA-Z0-9\.\-_]+$";
19+
20+
#if NET8_0_OR_GREATER
21+
[GeneratedRegex(KeyPattern, RegexOptions.None, matchTimeoutMilliseconds: 100)]
22+
private static partial Regex KeyRegex();
23+
#endif
2024

2125
/// <summary>
2226
/// All the fixed fields declared above are objects
@@ -54,12 +58,47 @@ private static void ValidateKeys(IValidationContext context, IEnumerable<string>
5458

5559
foreach (var key in keys)
5660
{
57-
if (!KeyRegex.IsMatch(key))
61+
#if NET8_0_OR_GREATER
62+
var isValidKey = KeyRegex().IsMatch(key);
63+
#else
64+
var isValidKey = IsValidKey(key);
65+
#endif
66+
if (!isValidKey)
5867
{
5968
context.CreateError(nameof(KeyMustBeRegularExpression),
60-
string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, component, KeyRegex.ToString()));
69+
string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, component, KeyPattern));
6170
}
6271
}
6372
}
73+
74+
internal static bool IsValidKey(string key)
75+
{
76+
// Match the fixed character class without wall-clock timeouts on downlevel targets.
77+
var length = key.Length;
78+
// The regex's $ anchor also accepts a single final newline.
79+
if (length > 0 && key[length - 1] == '\n')
80+
{
81+
length--;
82+
}
83+
84+
if (length == 0)
85+
{
86+
return false;
87+
}
88+
89+
for (var i = 0; i < length; i++)
90+
{
91+
var character = key[i];
92+
if (!(character >= 'a' && character <= 'z') &&
93+
!(character >= 'A' && character <= 'Z') &&
94+
!(character >= '0' && character <= '9') &&
95+
character != '.' && character != '-' && character != '_')
96+
{
97+
return false;
98+
}
99+
}
100+
101+
return true;
102+
}
64103
}
65104
}

‎test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs‎

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
// Licensed under the MIT license.
33

44
using System.Collections.Generic;
5+
using System.IO;
56
using System.Linq;
7+
using System.Text;
8+
using System.Text.Json;
9+
using System.Threading.Tasks;
610
using Xunit;
711

812
namespace Microsoft.OpenApi.Validations.Tests
@@ -32,8 +36,67 @@ public void ValidateKeyMustMatchRegularExpressionInComponents()
3236
Assert.False(result);
3337
Assert.NotNull(errors);
3438
var error = Assert.Single(errors);
35-
Assert.Equal(string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, "responses", OpenApiComponentsRules.KeyRegex.ToString()),
39+
Assert.Equal(string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, "responses", @"^[a-zA-Z0-9\.\-_]+$"),
3640
error.Message);
3741
}
42+
43+
[Theory]
44+
[InlineData("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_", true)]
45+
[InlineData("", false)]
46+
[InlineData("a b", false)]
47+
[InlineData("a/b", false)]
48+
[InlineData("é", false)]
49+
[InlineData("12", false)]
50+
[InlineData("a\n", true)]
51+
[InlineData("\n", false)]
52+
[InlineData("a\n\n", false)]
53+
[InlineData("a\r\n", false)]
54+
[InlineData("a\nb", false)]
55+
[InlineData("a\0", false)]
56+
public void ValidateComponentKeyPreservesRegexBehavior(string key, bool isValid)
57+
{
58+
var components = new OpenApiComponents
59+
{
60+
Schemas = new Dictionary<string, IOpenApiSchema>
61+
{
62+
{ key, new OpenApiSchema() }
63+
}
64+
};
65+
66+
var rules = new ValidationRuleSet();
67+
rules.Add(typeof(OpenApiComponents), OpenApiComponentsRules.KeyMustBeRegularExpression);
68+
var errors = components.Validate(rules);
69+
70+
Assert.Equal(isValid, OpenApiComponentsRules.IsValidKey(key));
71+
Assert.Equal(isValid, !errors.Any());
72+
}
73+
74+
[Theory]
75+
[InlineData(true)]
76+
[InlineData(false)]
77+
public async Task LoadAsyncValidatesLongComponentKeys(bool isValid)
78+
{
79+
var key = new string('a', 1_000_000) + (isValid ? string.Empty : "!");
80+
Assert.Equal(isValid, OpenApiComponentsRules.IsValidKey(key));
81+
var json = """
82+
{"openapi":"3.1.0","info":{"title":"Test","version":"1.0"},"paths":{},"components":{"schemas":{
83+
""" + JsonSerializer.Serialize(key) + ":{\"type\":\"string\"}}}}";
84+
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
85+
86+
var result = await OpenApiDocument.LoadAsync(stream, cancellationToken: TestContext.Current.CancellationToken);
87+
88+
Assert.NotNull(result.Document);
89+
Assert.NotNull(result.Diagnostic);
90+
if (isValid)
91+
{
92+
Assert.Empty(result.Diagnostic.Errors);
93+
}
94+
else
95+
{
96+
var error = Assert.Single(result.Diagnostic.Errors);
97+
Assert.Equal(string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr,
98+
key, "schemas", @"^[a-zA-Z0-9\.\-_]+$"), error.Message);
99+
}
100+
}
38101
}
39102
}

0 commit comments

Comments
 (0)