diff --git a/src/Microsoft.OpenApi/Reader/JsonNodeHelper.cs b/src/Microsoft.OpenApi/Reader/JsonNodeHelper.cs index d96c372ab..684e72c3b 100644 --- a/src/Microsoft.OpenApi/Reader/JsonNodeHelper.cs +++ b/src/Microsoft.OpenApi/Reader/JsonNodeHelper.cs @@ -172,6 +172,18 @@ public static Dictionary> CreateArrayMap(this JsonNode? no return jsonObject.TryGetPropertyValue("$ref", out var refNode) ? refNode?.GetScalarValue() : null; } + public static void ValidateReferencePointerFormat(string pointer) + { + var hashIndex = pointer.IndexOf('#'); + if (hashIndex < 0) return; + var slashIndex = pointer.IndexOf('/', hashIndex); + if (slashIndex >= 0 && + slashIndex != hashIndex + 1) + { + throw new OpenApiException(string.Format(SRResource.ReferenceHasInvalidFormat, pointer)); + } + } + /// /// Returns the value of $dynamicRef if $ref is absent. Used to create a schema reference /// for bare $dynamicRef schemas (no $ref) so they participate in reference resolution. diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs index 3996d8d7a..ab3d15e94 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -88,7 +88,7 @@ private static IOpenApiExtension LoadExtension(string name, JsonNode node, Parsi private static (string, string?) GetReferenceIdAndExternalResource(string pointer) { var refSegments = pointer.Split('/'); - var refId = refSegments[refSegments.Count() -1]; + var refId = refSegments[refSegments.Length - 1]; var isExternalResource = !refSegments[0].StartsWith("#", StringComparison.OrdinalIgnoreCase); string? externalResource = isExternalResource ? $"{refSegments[0]}/{refSegments[1].TrimEnd('#')}" : null; diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiV3Deserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiV3Deserializer.cs index e7ca30d39..1e281afb0 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiV3Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiV3Deserializer.cs @@ -152,8 +152,10 @@ private static IOpenApiExtension LoadExtension(string name, JsonNode node, Parsi private static (string, string?) GetReferenceIdAndExternalResource(string pointer) { + JsonNodeHelper.ValidateReferencePointerFormat(pointer); + var refSegments = pointer.Split('/'); - var refId = refSegments[refSegments.Count() -1]; + var refId = refSegments[refSegments.Length - 1]; var isExternalResource = !refSegments[0].StartsWith("#", StringComparison.OrdinalIgnoreCase); string? externalResource = null; diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs index 81a7811ff..617ee9ced 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -152,6 +152,8 @@ private static IOpenApiExtension LoadExtension(string name, JsonNode node, Parsi private static (string, string?) GetReferenceIdAndExternalResource(string pointer) { + JsonNodeHelper.ValidateReferencePointerFormat(pointer); + /* Check whether the reference pointer is a URL * (id keyword allows you to supply a URL for the schema as a target for referencing) * E.g. $ref: 'https://example.com/schemas/resource.json' @@ -159,7 +161,7 @@ private static (string, string?) GetReferenceIdAndExternalResource(string pointe * E.g. $ref: '#/components/schemas/pet' */ var refSegments = pointer.Split('/'); - string refId = !pointer.Contains('#') ? pointer : refSegments[refSegments.Count()-1]; + string refId = !pointer.Contains('#') ? pointer : refSegments[refSegments.Length - 1]; var isExternalResource = !refSegments[0].StartsWith("#", StringComparison.OrdinalIgnoreCase); string? externalResource = null; diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs index 4031b45a1..5efed4657 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -152,6 +152,8 @@ private static IOpenApiExtension LoadExtension(string name, JsonNode node, Parsi private static (string, string?) GetReferenceIdAndExternalResource(string pointer) { + JsonNodeHelper.ValidateReferencePointerFormat(pointer); + /* Check whether the reference pointer is a URL * (id keyword allows you to supply a URL for the schema as a target for referencing) * E.g. $ref: 'https://example.com/schemas/resource.json' @@ -159,7 +161,7 @@ private static (string, string?) GetReferenceIdAndExternalResource(string pointe * E.g. $ref: '#/components/schemas/pet' */ var refSegments = pointer.Split('/'); - string refId = !pointer.Contains('#') ? pointer : refSegments[refSegments.Count()-1]; + string refId = !pointer.Contains('#') ? pointer : refSegments[refSegments.Length - 1]; var isExternalResource = !refSegments[0].StartsWith("#", StringComparison.OrdinalIgnoreCase); string? externalResource = null; diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs index 8acf84d09..f764f7e60 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs @@ -16,6 +16,40 @@ public class OpenApiDocumentTests { private const string SampleFolderPath = "V31Tests/Samples/OpenApiDocument/"; + [Fact] + public void ParseDocumentWithMalformedReferenceShouldYieldExpectedDiagnostic() + { + var result = OpenApiDocument.Parse( + """ + openapi: 3.1.1 + info: + title: test + version: 1.0.0 + paths: + /test: + get: + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#components/schemas/Item' + components: + schemas: + Item: + type: object + properties: + id: + type: integer + """, + OpenApiConstants.Yaml, + SettingsFixture.ReaderSettings); + + var error = Assert.Single(result.Diagnostic.Errors); + Assert.Equal("The reference string '#components/schemas/Item' has invalid format.", error.Message); + } + [Fact] public async Task ParseDocumentWithWebhooksShouldSucceed() { diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.cs index 80051f05c..79f2d990f 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.cs @@ -16,6 +16,40 @@ public class OpenApiDocumentTests { private const string SampleFolderPath = "V32Tests/Samples/OpenApiDocument/"; + [Fact] + public void ParseDocumentWithMalformedReferenceShouldYieldExpectedDiagnostic() + { + var result = OpenApiDocument.Parse( + """ + openapi: 3.2.0 + info: + title: test + version: 1.0.0 + paths: + /test: + get: + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#components/schemas/Item' + components: + schemas: + Item: + type: object + properties: + id: + type: integer + """, + OpenApiConstants.Yaml, + SettingsFixture.ReaderSettings); + + var error = Assert.Single(result.Diagnostic.Errors); + Assert.Equal("The reference string '#components/schemas/Item' has invalid format.", error.Message); + } + [Fact] public async Task ParseDocumentWithWebhooksShouldSucceed() { @@ -665,4 +699,3 @@ public void LoadDocumentWithBooleanSchemaShouldNotThrowNullReferenceException() } } } - diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs index 2e278ca0e..53e3802ed 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs @@ -153,6 +153,40 @@ public async Task ParseBrokenMinimalDocumentShouldYieldExpectedDiagnostic() }, result.Diagnostic); } + [Fact] + public void ParseDocumentWithMalformedReferenceShouldYieldExpectedDiagnostic() + { + var result = OpenApiDocument.Parse( + """ + openapi: 3.0.3 + info: + title: test + version: 1.0.0 + paths: + /test: + get: + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#components/schemas/Item' + components: + schemas: + Item: + type: object + properties: + id: + type: integer + """, + OpenApiConstants.Yaml, + SettingsFixture.ReaderSettings); + + var error = Assert.Single(result.Diagnostic.Errors); + Assert.Equal("The reference string '#components/schemas/Item' has invalid format.", error.Message); + } + [Fact] public async Task ParseMinimalDocumentShouldSucceed() {