Skip to content

Commit 63fc55d

Browse files
authored
Merge pull request #2946 from microsoft/fix/pointer-validation
fix: adds explicit error message for invalid json pointers
2 parents f692e45 + 527a2cf commit 63fc55d

8 files changed

Lines changed: 127 additions & 8 deletions

File tree

src/Microsoft.OpenApi/Reader/JsonNodeHelper.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,18 @@ public static Dictionary<string, HashSet<T>> CreateArrayMap<T>(this JsonNode? no
172172
return jsonObject.TryGetPropertyValue("$ref", out var refNode) ? refNode?.GetScalarValue() : null;
173173
}
174174

175+
public static void ValidateReferencePointerFormat(string pointer)
176+
{
177+
var hashIndex = pointer.IndexOf('#');
178+
if (hashIndex < 0) return;
179+
var slashIndex = pointer.IndexOf('/', hashIndex);
180+
if (slashIndex >= 0 &&
181+
slashIndex != hashIndex + 1)
182+
{
183+
throw new OpenApiException(string.Format(SRResource.ReferenceHasInvalidFormat, pointer));
184+
}
185+
}
186+
175187
/// <summary>
176188
/// Returns the value of $dynamicRef if $ref is absent. Used to create a schema reference
177189
/// for bare $dynamicRef schemas (no $ref) so they participate in reference resolution.

src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs

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

44
using System;
@@ -88,7 +88,7 @@ private static IOpenApiExtension LoadExtension(string name, JsonNode node, Parsi
8888
private static (string, string?) GetReferenceIdAndExternalResource(string pointer)
8989
{
9090
var refSegments = pointer.Split('/');
91-
var refId = refSegments[refSegments.Count() -1];
91+
var refId = refSegments[refSegments.Length - 1];
9292
var isExternalResource = !refSegments[0].StartsWith("#", StringComparison.OrdinalIgnoreCase);
9393

9494
string? externalResource = isExternalResource ? $"{refSegments[0]}/{refSegments[1].TrimEnd('#')}" : null;

src/Microsoft.OpenApi/Reader/V3/OpenApiV3Deserializer.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,10 @@ private static IOpenApiExtension LoadExtension(string name, JsonNode node, Parsi
152152

153153
private static (string, string?) GetReferenceIdAndExternalResource(string pointer)
154154
{
155+
JsonNodeHelper.ValidateReferencePointerFormat(pointer);
156+
155157
var refSegments = pointer.Split('/');
156-
var refId = refSegments[refSegments.Count() -1];
158+
var refId = refSegments[refSegments.Length - 1];
157159
var isExternalResource = !refSegments[0].StartsWith("#", StringComparison.OrdinalIgnoreCase);
158160

159161
string? externalResource = null;

src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs

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

44
using System;
@@ -152,14 +152,16 @@ private static IOpenApiExtension LoadExtension(string name, JsonNode node, Parsi
152152

153153
private static (string, string?) GetReferenceIdAndExternalResource(string pointer)
154154
{
155+
JsonNodeHelper.ValidateReferencePointerFormat(pointer);
156+
155157
/* Check whether the reference pointer is a URL
156158
* (id keyword allows you to supply a URL for the schema as a target for referencing)
157159
* E.g. $ref: 'https://example.com/schemas/resource.json'
158160
* or its a normal json pointer fragment syntax
159161
* E.g. $ref: '#/components/schemas/pet'
160162
*/
161163
var refSegments = pointer.Split('/');
162-
string refId = !pointer.Contains('#') ? pointer : refSegments[refSegments.Count()-1];
164+
string refId = !pointer.Contains('#') ? pointer : refSegments[refSegments.Length - 1];
163165

164166
var isExternalResource = !refSegments[0].StartsWith("#", StringComparison.OrdinalIgnoreCase);
165167
string? externalResource = null;

src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs

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

44
using System;
@@ -152,14 +152,16 @@ private static IOpenApiExtension LoadExtension(string name, JsonNode node, Parsi
152152

153153
private static (string, string?) GetReferenceIdAndExternalResource(string pointer)
154154
{
155+
JsonNodeHelper.ValidateReferencePointerFormat(pointer);
156+
155157
/* Check whether the reference pointer is a URL
156158
* (id keyword allows you to supply a URL for the schema as a target for referencing)
157159
* E.g. $ref: 'https://example.com/schemas/resource.json'
158160
* or its a normal json pointer fragment syntax
159161
* E.g. $ref: '#/components/schemas/pet'
160162
*/
161163
var refSegments = pointer.Split('/');
162-
string refId = !pointer.Contains('#') ? pointer : refSegments[refSegments.Count()-1];
164+
string refId = !pointer.Contains('#') ? pointer : refSegments[refSegments.Length - 1];
163165

164166
var isExternalResource = !refSegments[0].StartsWith("#", StringComparison.OrdinalIgnoreCase);
165167
string? externalResource = null;

test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,40 @@ public class OpenApiDocumentTests
1616
{
1717
private const string SampleFolderPath = "V31Tests/Samples/OpenApiDocument/";
1818

19+
[Fact]
20+
public void ParseDocumentWithMalformedReferenceShouldYieldExpectedDiagnostic()
21+
{
22+
var result = OpenApiDocument.Parse(
23+
"""
24+
openapi: 3.1.1
25+
info:
26+
title: test
27+
version: 1.0.0
28+
paths:
29+
/test:
30+
get:
31+
responses:
32+
'200':
33+
description: successful operation
34+
content:
35+
application/json:
36+
schema:
37+
$ref: '#components/schemas/Item'
38+
components:
39+
schemas:
40+
Item:
41+
type: object
42+
properties:
43+
id:
44+
type: integer
45+
""",
46+
OpenApiConstants.Yaml,
47+
SettingsFixture.ReaderSettings);
48+
49+
var error = Assert.Single(result.Diagnostic.Errors);
50+
Assert.Equal("The reference string '#components/schemas/Item' has invalid format.", error.Message);
51+
}
52+
1953
[Fact]
2054
public async Task ParseDocumentWithWebhooksShouldSucceed()
2155
{

test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,40 @@ public class OpenApiDocumentTests
1616
{
1717
private const string SampleFolderPath = "V32Tests/Samples/OpenApiDocument/";
1818

19+
[Fact]
20+
public void ParseDocumentWithMalformedReferenceShouldYieldExpectedDiagnostic()
21+
{
22+
var result = OpenApiDocument.Parse(
23+
"""
24+
openapi: 3.2.0
25+
info:
26+
title: test
27+
version: 1.0.0
28+
paths:
29+
/test:
30+
get:
31+
responses:
32+
'200':
33+
description: successful operation
34+
content:
35+
application/json:
36+
schema:
37+
$ref: '#components/schemas/Item'
38+
components:
39+
schemas:
40+
Item:
41+
type: object
42+
properties:
43+
id:
44+
type: integer
45+
""",
46+
OpenApiConstants.Yaml,
47+
SettingsFixture.ReaderSettings);
48+
49+
var error = Assert.Single(result.Diagnostic.Errors);
50+
Assert.Equal("The reference string '#components/schemas/Item' has invalid format.", error.Message);
51+
}
52+
1953
[Fact]
2054
public async Task ParseDocumentWithWebhooksShouldSucceed()
2155
{
@@ -665,4 +699,3 @@ public void LoadDocumentWithBooleanSchemaShouldNotThrowNullReferenceException()
665699
}
666700
}
667701
}
668-

test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,40 @@ public async Task ParseBrokenMinimalDocumentShouldYieldExpectedDiagnostic()
153153
}, result.Diagnostic);
154154
}
155155

156+
[Fact]
157+
public void ParseDocumentWithMalformedReferenceShouldYieldExpectedDiagnostic()
158+
{
159+
var result = OpenApiDocument.Parse(
160+
"""
161+
openapi: 3.0.3
162+
info:
163+
title: test
164+
version: 1.0.0
165+
paths:
166+
/test:
167+
get:
168+
responses:
169+
'200':
170+
description: successful operation
171+
content:
172+
application/json:
173+
schema:
174+
$ref: '#components/schemas/Item'
175+
components:
176+
schemas:
177+
Item:
178+
type: object
179+
properties:
180+
id:
181+
type: integer
182+
""",
183+
OpenApiConstants.Yaml,
184+
SettingsFixture.ReaderSettings);
185+
186+
var error = Assert.Single(result.Diagnostic.Errors);
187+
Assert.Equal("The reference string '#components/schemas/Item' has invalid format.", error.Message);
188+
}
189+
156190
[Fact]
157191
public async Task ParseMinimalDocumentShouldSucceed()
158192
{

0 commit comments

Comments
 (0)