From 2efd2b2e48be072f092115dface6cea3e0ba7182 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 20 Mar 2026 11:50:42 -0400 Subject: [PATCH] Merge pull request #2790 from microsoft/fix/path-parameter-with-slash-validation fix: a bug where path parameter validation would fail if they contained forbidden JSON pointer characters --- .../Services/OpenApiVisitorBase.cs | 13 +++- .../Rules/OpenApiParameterRules.cs | 2 +- .../OpenApiHeaderValidationTests.cs | 2 +- .../OpenApiParameterValidationTests.cs | 69 ++++++++++++++++++- .../OpenApiSchemaValidationTests.cs | 6 +- 5 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs index 4c3419765..c00a54888 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs @@ -33,10 +33,19 @@ public virtual void Enter(string segment) this._path.Push(string.Empty); return; } + this._path.Push(EncodeJsonPointerSegment(segment)); + } + + internal static string EncodeJsonPointerSegment(string? segment) + { + if (string.IsNullOrEmpty(segment)) + { + return string.Empty; + } #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER - this._path.Push(segment.Replace("~", "~0", StringComparison.Ordinal).Replace("/", "~1", StringComparison.OrdinalIgnoreCase)); + return segment.Replace("~", "~0", StringComparison.Ordinal).Replace("/", "~1", StringComparison.OrdinalIgnoreCase); #else - this._path.Push(segment.Replace("~", "~0").Replace("/", "~1")); + return segment!.Replace("~", "~0").Replace("/", "~1"); #endif } diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs index a84ea3255..c00e1dcce 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs @@ -61,7 +61,7 @@ public static class OpenApiParameterRules (context, parameter) => { if (parameter.In == ParameterLocation.Path && - !(context.PathString.Contains("{" + parameter.Name + "}") || context.PathString.Contains("#/components"))) + !(context.PathString.Contains("{" + OpenApiVisitorBase.EncodeJsonPointerSegment(parameter.Name) + "}") || context.PathString.Contains("#/components"))) { context.Enter("in"); context.CreateError( diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiHeaderValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiHeaderValidationTests.cs index a7a0cba0a..98e59928c 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiHeaderValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiHeaderValidationTests.cs @@ -86,7 +86,7 @@ public void ValidateExamplesShouldNotHaveDataTypeMismatchForSimpleSchema() // Act var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); var walker = new OpenApiWalker(validator); - walker.Walk(header); + walker.Walk((IOpenApiHeader)header); warnings = validator.Warnings; var result = !warnings.Any(); diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs index 207db74b6..1d2882033 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs @@ -75,7 +75,7 @@ public void ValidateExampleShouldNotHaveDataTypeMismatchForSimpleSchema() var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); validator.Enter("{parameter1}"); var walker = new OpenApiWalker(validator); - walker.Walk(parameter); + walker.Walk((IOpenApiParameter)parameter); warnings = validator.Warnings; var result = !warnings.Any(); @@ -203,7 +203,7 @@ public void PathParameterInThePathShouldBeOk() validator.Enter("1"); var walker = new OpenApiWalker(validator); - walker.Walk(parameter); + walker.Walk((IOpenApiParameter)parameter); errors = validator.Errors; var result = errors.Any(); @@ -211,5 +211,70 @@ public void PathParameterInThePathShouldBeOk() // Assert Assert.False(result); } + + [Fact] + public void PathParameterInThePathShouldBeOkWithSlashInParameterName() + { + // Arrange + IEnumerable errors; + + var parameter = new OpenApiParameter + { + Name = "parameter/1", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.String, + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + validator.Enter("paths"); + validator.Enter("/{parameter/1}"); + validator.Enter("get"); + validator.Enter("parameters"); + validator.Enter("1"); + + var walker = new OpenApiWalker(validator); + walker.Walk((IOpenApiParameter)parameter); + + errors = validator.Errors; + var result = errors.Any(); + + // Assert + Assert.False(result); + } + + [Fact] + public void PathParameterValidationShouldNotThrowWithEmptyParameterName() + { + // Arrange + var parameter = new OpenApiParameter + { + Name = string.Empty, + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.String, + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + validator.Enter("paths"); + validator.Enter("/{}"); + validator.Enter("get"); + validator.Enter("parameters"); + validator.Enter("1"); + + var walker = new OpenApiWalker(validator); + var exception = Record.Exception(() => walker.Walk((IOpenApiParameter)parameter)); + + // Assert + Assert.Null(exception); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs index fdf36e0b4..0cb229534 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs @@ -26,7 +26,7 @@ public void ValidateDefaultShouldNotHaveDataTypeMismatchForSimpleSchema() // Act var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); var walker = new OpenApiWalker(validator); - walker.Walk(schema); + walker.Walk((IOpenApiSchema)schema); warnings = validator.Warnings; var result = !warnings.Any(); @@ -50,7 +50,7 @@ public void ValidateExampleAndDefaultShouldNotHaveDataTypeMismatchForSimpleSchem // Act var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); var walker = new OpenApiWalker(validator); - walker.Walk(schema); + walker.Walk((IOpenApiSchema)schema); warnings = validator.Warnings; bool result = !warnings.Any(); @@ -92,7 +92,7 @@ public void ValidateEnumShouldNotHaveDataTypeMismatchForSimpleSchema() // Act var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); var walker = new OpenApiWalker(validator); - walker.Walk(schema); + walker.Walk((IOpenApiSchema)schema); warnings = validator.Warnings; var result = !warnings.Any();