Skip to content

Commit 9b81767

Browse files
fredbiclaude
andauthored
feat: inject a custom document loader into schema/spec validation (#272)
Building a schema validator expands the schema's $ref, and validating a specification resolves and expands $ref (schemas, parameters, responses, and the whole-spec expansion). All of these resolved documents through the spec package's global loader, with no way for a caller to supply their own. Add WithPathLoader, an option-aware loader on SchemaValidatorOptions, consumed by every path that loads documents during validation: - newSchemaValidator uses spec.ExpandSchemaWithOptions (instead of the default-settings ExpandSchema). - resolveParam / expandResponseRef use spec.ExpandParameterWithOptions and spec.ExpandResponseWithOptions. - SpecValidator.resolveRef passes the loader through spec.ResolveRefWithBase. - validateReferencesValid passes it to loads.Document.Expanded (no-op when unset: loads falls back to the document's own loader). - NewSpecValidator and the package-level Spec now accept ...Option, applied to the internal schema options after the built-in defaults, so a caller can inject the loader into spec validation. Backward compatible: existing two-argument calls are unaffected. The injected loader may carry any go-openapi/swag/loading options — a custom HTTP client or timeout, authentication or custom headers, an embedded or rooted file system, and so on. One important use is confining loading of untrusted input (loading.WithRoot to confine local reads, loading.WithHTTPClient to restrict remote fetches, or a restricted loader from go-openapi/loads), which closes the arbitrary-file-read / SSRF surface that the unsandboxed default otherwise exposes. Left unset, behavior is unchanged. Bumps spec to v0.22.9 (for the option-aware expanders), analysis to v0.25.5, loads to v0.25.0 and swag to v0.27.3. Signed-off-by: Frederic BIDON <fredbi@yahoo.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6614383 commit 9b81767

7 files changed

Lines changed: 162 additions & 58 deletions

File tree

go.mod

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
module github.com/go-openapi/validate
22

33
require (
4-
github.com/go-openapi/analysis v0.25.3
4+
github.com/go-openapi/analysis v0.25.5
55
github.com/go-openapi/errors v0.22.8
66
github.com/go-openapi/jsonpointer v1.0.0
7-
github.com/go-openapi/loads v0.24.0
8-
github.com/go-openapi/spec v0.22.6
7+
github.com/go-openapi/loads v0.25.0
8+
github.com/go-openapi/spec v0.22.9
99
github.com/go-openapi/strfmt v0.27.0
10-
github.com/go-openapi/swag/conv v0.27.0
11-
github.com/go-openapi/swag/fileutils v0.27.0
12-
github.com/go-openapi/swag/jsonutils v0.27.0
13-
github.com/go-openapi/swag/stringutils v0.27.0
10+
github.com/go-openapi/swag/conv v0.27.3
11+
github.com/go-openapi/swag/fileutils v0.27.3
12+
github.com/go-openapi/swag/jsonutils v0.27.3
13+
github.com/go-openapi/swag/loading v0.27.3
14+
github.com/go-openapi/swag/stringutils v0.27.3
1415
github.com/go-openapi/testify/v2 v2.6.0
1516
go.yaml.in/yaml/v3 v3.0.4
1617
)
1718

1819
require (
19-
github.com/go-openapi/jsonreference v0.21.6 // indirect
20-
github.com/go-openapi/swag/jsonname v0.26.1 // indirect
21-
github.com/go-openapi/swag/loading v0.26.1 // indirect
22-
github.com/go-openapi/swag/mangling v0.26.1 // indirect
23-
github.com/go-openapi/swag/typeutils v0.27.0 // indirect
24-
github.com/go-openapi/swag/yamlutils v0.26.1 // indirect
20+
github.com/go-openapi/jsonreference v1.0.0 // indirect
21+
github.com/go-openapi/swag/mangling v0.27.3 // indirect
22+
github.com/go-openapi/swag/pools v0.27.3 // indirect
23+
github.com/go-openapi/swag/typeutils v0.27.3 // indirect
24+
github.com/go-openapi/swag/yamlutils v0.27.3 // indirect
2525
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
2626
github.com/google/uuid v1.6.0 // indirect
2727
github.com/oklog/ulid/v2 v2.1.1 // indirect

go.sum

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,37 @@
1-
github.com/go-openapi/analysis v0.25.3 h1:4zlcg85pd2xq3sEgjW887n1IpwCpCqTmqeT6dP9OxDw=
2-
github.com/go-openapi/analysis v0.25.3/go.mod h1:6PEmUIra9/rn6SPstzbrMkhFAsMB2qm7g6E+4DRFyCU=
1+
github.com/go-openapi/analysis v0.25.5 h1:xPYEvTb90o1y0epuiOPAoG4QqahjP3cdp5xNlHeKJRI=
2+
github.com/go-openapi/analysis v0.25.5/go.mod h1:d3UGtQC5uq5Kqqqis2VH09Km/v3vwsWrYkbp4gdm+Rc=
33
github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I=
44
github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w=
55
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
66
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
7-
github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y=
8-
github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY=
9-
github.com/go-openapi/loads v0.24.0 h1:4LLorXRPTzIN9V6ngMUZbAscsBOUBk3Oa8cClu/bFrQ=
10-
github.com/go-openapi/loads v0.24.0/go.mod h1:xQMgX+hw5xRAhGrcDXxeMw78IFqUpIzhleu3HqPhyF4=
11-
github.com/go-openapi/spec v0.22.6 h1:Tyy1pLaNCM8GBCFLoGYLonjJi6zykqyLCjXLc19ZPic=
12-
github.com/go-openapi/spec v0.22.6/go.mod h1:HZvTHat+iH0PALQRWhrqIHtU/PEqxqd89fu0MxGlMeM=
7+
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
8+
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
9+
github.com/go-openapi/loads v0.25.0 h1:74Bc2snfaVlsHzwdQj/3gsA9XJz3daXTJVs+4ZaK7jI=
10+
github.com/go-openapi/loads v0.25.0/go.mod h1:JFBw4SIB9+PTIFHDfcXuSSy5h6aWzjtUCrPYyx3qWU8=
11+
github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w=
12+
github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0=
1313
github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM=
1414
github.com/go-openapi/strfmt v0.27.0/go.mod h1:s/qhDqfY72irigXUGJmtgid2Rm+3tnz3k8hZaRmvWYc=
15-
github.com/go-openapi/swag/conv v0.27.0 h1:EKOH4feXrvdo8DbSsXSAqRT8fz1epEnS5O2IfXUOzE8=
16-
github.com/go-openapi/swag/conv v0.27.0/go.mod h1:pfiv0uKQTbaGApk8Zs/lZV3uSjmSpa2FO1y183YngN8=
17-
github.com/go-openapi/swag/fileutils v0.27.0 h1:ib5jMUqGq5tY1EyO4inlrabsaeDAleFU+XD1FXQcgp8=
18-
github.com/go-openapi/swag/fileutils v0.27.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
19-
github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE=
20-
github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc=
21-
github.com/go-openapi/swag/jsonutils v0.27.0 h1:VYtd9jEQYeU4j8q5vdn5KWotF4vKywhGdMBrALtAsfE=
22-
github.com/go-openapi/swag/jsonutils v0.27.0/go.mod h1:U7pb8AGuwhok3RDicHeHwSG4L3PXSq6PAL98Aon632g=
23-
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0 h1:+d7C7Ur/SsGg/UZ9G0JEovnfRqtMNZCJQGKc2h/ojoE=
24-
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
25-
github.com/go-openapi/swag/loading v0.26.1 h1:E9K4wqXeROlhjFQ13K9zMz6ojFGXIggGe+ad1odrK9w=
26-
github.com/go-openapi/swag/loading v0.26.1/go.mod h1:3qvRIlWzWdq1HvmldwmuJ2ohpcAryN6xVt2OTKd0/7E=
27-
github.com/go-openapi/swag/mangling v0.26.1 h1:gpYI4WuPKFJJVjV5cDLGlDVJhFIxYjQc7yN5eEb4CqM=
28-
github.com/go-openapi/swag/mangling v0.26.1/go.mod h1:POETDH01hqAdASXfw7ISEd9bCOE6xBHOt8NHmGZRmYM=
29-
github.com/go-openapi/swag/stringutils v0.27.0 h1:Of7w/HljWsNZvuxsUAnw3n+hCOyI6HLJOxW2kQRAxio=
30-
github.com/go-openapi/swag/stringutils v0.27.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
31-
github.com/go-openapi/swag/typeutils v0.27.0 h1:aCf4MSGo8NLwZP8Q6t32DWLJSvl/WwNqgmEG+xJ6v2o=
32-
github.com/go-openapi/swag/typeutils v0.27.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
33-
github.com/go-openapi/swag/yamlutils v0.26.1 h1:0TSLK+lXs9vfIhAWzBeI/lOzEnIoot6WTCO1aAeWFTk=
34-
github.com/go-openapi/swag/yamlutils v0.26.1/go.mod h1:7W5b7PRX9MxwL7TjeG7H8HkyBGRsIDRObhyMWFgBI2M=
15+
github.com/go-openapi/swag/conv v0.27.3 h1:iqJFmGEjmX3AY0lSszABFqRVqOSt99XS0LzNIMJYuhU=
16+
github.com/go-openapi/swag/conv v0.27.3/go.mod h1:nPRmN6jgNme99hpf+nM0auDZGALWIqlwhisKPK/bQhQ=
17+
github.com/go-openapi/swag/fileutils v0.27.3 h1:3UVoZ2RLaIs1lt+2jcKzL8RM3Yk0rmsDE9FLA/HGxFE=
18+
github.com/go-openapi/swag/fileutils v0.27.3/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
19+
github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc=
20+
github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo=
21+
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8=
22+
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
23+
github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0=
24+
github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE=
25+
github.com/go-openapi/swag/mangling v0.27.3 h1:gRzzD1PAUoLTtGMgI3KpBmCSOlTuLTFWnviLxLcTnyg=
26+
github.com/go-openapi/swag/mangling v0.27.3/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
27+
github.com/go-openapi/swag/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk=
28+
github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
29+
github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI=
30+
github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
31+
github.com/go-openapi/swag/typeutils v0.27.3 h1:l6SSrx5eR5/WVwrGNzN6bQ9WqL04mrxNBl9YgQ3rcJ4=
32+
github.com/go-openapi/swag/typeutils v0.27.3/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
33+
github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs=
34+
github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU=
3535
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
3636
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
3737
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=

helpers.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,9 @@ func (h *paramHelper) resolveParam(path, method, operationID string, param *spec
242242
res := new(Result)
243243
isRef := param.Ref.String() != ""
244244
if s.spec.SpecFilePath() == "" {
245-
err = spec.ExpandParameterWithRoot(param, s.spec.Spec(), nil)
245+
err = spec.ExpandParameterWithOptions(param, s.spec.Spec(), nil, s.schemaOptions.expandOptions(""))
246246
} else {
247-
err = spec.ExpandParameter(param, s.spec.SpecFilePath())
247+
err = spec.ExpandParameterWithOptions(param, nil, nil, s.schemaOptions.expandOptions(s.spec.SpecFilePath()))
248248
}
249249
if err != nil { // Safeguard
250250
// NOTE: we may enter here when the whole parameter is an unresolved $ref
@@ -295,9 +295,9 @@ func (r *responseHelper) expandResponseRef(
295295
res := new(Result)
296296
if s.spec.SpecFilePath() == "" {
297297
// there is no physical document to resolve $ref in response
298-
err = spec.ExpandResponseWithRoot(response, s.spec.Spec(), nil)
298+
err = spec.ExpandResponseWithOptions(response, s.spec.Spec(), nil, s.schemaOptions.expandOptions(""))
299299
} else {
300-
err = spec.ExpandResponse(response, s.spec.SpecFilePath())
300+
err = spec.ExpandResponseWithOptions(response, nil, nil, s.schemaOptions.expandOptions(s.spec.SpecFilePath()))
301301
}
302302
if err != nil { // Safeguard
303303
// NOTE: we may enter here when the whole response is an unresolved $ref.

schema.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,18 @@ func newSchemaValidator(schema *spec.Schema, rootSchema any, root string, format
6363
rootSchema = schema
6464
}
6565

66+
if opts == nil {
67+
opts = new(SchemaValidatorOptions)
68+
}
69+
6670
if schema.ID != "" || schema.Ref.String() != "" || schema.Ref.IsRoot() {
67-
err := spec.ExpandSchema(schema, rootSchema, nil)
71+
err := spec.ExpandSchemaWithOptions(schema, rootSchema, nil, opts.expandOptions(""))
6872
if err != nil {
6973
msg := invalidSchemaProvidedMsg(err).Error()
7074
panic(msg)
7175
}
7276
}
7377

74-
if opts == nil {
75-
opts = new(SchemaValidatorOptions)
76-
}
77-
7878
var s *SchemaValidator
7979
if opts.recycleValidators {
8080
s = pools.poolOfSchemaValidators.BorrowValidator()

schema_loader_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package validate
5+
6+
import (
7+
"encoding/json"
8+
"net/http"
9+
"net/http/httptest"
10+
"testing"
11+
12+
"github.com/go-openapi/spec"
13+
"github.com/go-openapi/strfmt"
14+
"github.com/go-openapi/swag/loading"
15+
"github.com/go-openapi/testify/v2/assert"
16+
"github.com/go-openapi/testify/v2/require"
17+
)
18+
19+
// TestSchemaValidator_WithPathLoader verifies that a document loader injected through
20+
// WithPathLoader is the one used to resolve a remote $ref when building a schema validator — the
21+
// hook a caller uses to confine loading when validating input from an untrusted source.
22+
func TestSchemaValidator_WithPathLoader(t *testing.T) {
23+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
24+
_, _ = w.Write([]byte(`{"type":"string"}`))
25+
}))
26+
defer srv.Close()
27+
28+
var calls int
29+
loader := func(pth string, opts ...loading.Option) (json.RawMessage, error) {
30+
calls++
31+
return loading.LoadFromFileOrHTTP(pth, opts...)
32+
}
33+
34+
schema := &spec.Schema{SchemaProps: spec.SchemaProps{Ref: spec.MustCreateRef(srv.URL + "/schema.json")}}
35+
v := NewSchemaValidator(schema, nil, "", strfmt.Default, WithPathLoader(loader))
36+
37+
require.NotNil(t, v)
38+
assert.TrueT(t, calls > 0, "expected the injected loader to resolve the remote $ref")
39+
}
40+
41+
// TestSpecValidator_WithPathLoader verifies that WithPathLoader passed to the spec validator
42+
// constructors reaches the schema options consumed by both the schema validation and the $ref
43+
// resolution ($ref resolveRef) performed during spec validation.
44+
func TestSpecValidator_WithPathLoader(t *testing.T) {
45+
loader := func(string, ...loading.Option) (json.RawMessage, error) { return nil, nil }
46+
47+
s := NewSpecValidator(spec.MustLoadSwagger20Schema(), strfmt.Default, WithPathLoader(loader))
48+
require.NotNil(t, s.schemaOptions)
49+
require.NotNil(t, s.schemaOptions.pathLoaderWithOptions,
50+
"WithPathLoader must reach the spec validator's schema options (used by resolveRef and schema validation)")
51+
52+
// built-in defaults are still applied alongside the injected option
53+
assert.TrueT(t, s.schemaOptions.EnableObjectArrayTypeCheck)
54+
assert.TrueT(t, s.schemaOptions.recycleValidators)
55+
}

schema_option.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@
33

44
package validate
55

6+
import (
7+
"encoding/json"
8+
9+
"github.com/go-openapi/spec"
10+
"github.com/go-openapi/swag/loading"
11+
)
12+
613
// SchemaValidatorOptions defines optional rules for schema validation.
714
type SchemaValidatorOptions struct {
815
EnableObjectArrayTypeCheck bool
916
EnableArrayMustHaveItemsCheck bool
1017
recycleValidators bool
1118
recycleResult bool
1219
skipSchemataResult bool
20+
pathLoaderWithOptions func(string, ...loading.Option) (json.RawMessage, error)
1321
}
1422

1523
// Option sets optional rules for schema validation.
@@ -60,6 +68,23 @@ func WithSkipSchemataResult(enable bool) Option {
6068
}
6169
}
6270

71+
// WithPathLoader injects the document loader used to resolve remote and relative $ref while
72+
// validating a schema or specification. It matches the option-aware loader signature of
73+
// github.com/go-openapi/swag/loading (and go-openapi/loads).
74+
//
75+
// This lets validation resolve references through a caller-provided loader instead of the spec
76+
// package's global default. The loader may carry any loading options — a custom HTTP client or
77+
// timeout, authentication or custom headers, an embedded or rooted file system, and so on.
78+
//
79+
// One important use is confining loading of untrusted input: build the loader with loading.WithRoot
80+
// (to confine local reads) and loading.WithHTTPClient (to restrict remote fetches), or use a
81+
// restricted loader from go-openapi/loads. Left unset, the spec package default loader is used.
82+
func WithPathLoader(loader func(string, ...loading.Option) (json.RawMessage, error)) Option {
83+
return func(svo *SchemaValidatorOptions) {
84+
svo.pathLoaderWithOptions = loader
85+
}
86+
}
87+
6388
// Options returns the current set of options.
6489
func (svo SchemaValidatorOptions) Options() []Option {
6590
return []Option{
@@ -68,5 +93,17 @@ func (svo SchemaValidatorOptions) Options() []Option {
6893
WithRecycleValidators(svo.recycleValidators),
6994
withRecycleResults(svo.recycleResult),
7095
WithSkipSchemataResult(svo.skipSchemataResult),
96+
WithPathLoader(svo.pathLoaderWithOptions),
97+
}
98+
}
99+
100+
// expandOptions builds the spec expand options for schema/$ref expansion during validation,
101+
// carrying the injected loader (when set) so resolution can be confined. relativeBase is used for
102+
// base-path-relative resolution; it is ignored by [spec.ExpandSchemaWithOptions], which derives the
103+
// base from the root.
104+
func (svo *SchemaValidatorOptions) expandOptions(relativeBase string) *spec.ExpandOptions {
105+
return &spec.ExpandOptions{
106+
RelativeBase: relativeBase,
107+
PathLoaderWithOptions: svo.pathLoaderWithOptions,
71108
}
72109
}

spec.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import (
2525
//
2626
// Returns an error flattening in a single standard error, all validation messages.
2727
//
28+
// Options are forwarded to the underlying [SpecValidator]; in particular [WithPathLoader] injects a
29+
// confined document loader for validating a specification from an untrusted source.
30+
//
2831
// - Proposal for enhancement: $ref should not have siblings
2932
// - Proposal for enhancement: make sure documentation reflects all checks and warnings
3033
// - Proposal for enhancement: check on discriminators
@@ -35,8 +38,8 @@ import (
3538
// - Proposal for enhancement: check on required properties to support anyOf, allOf, oneOf
3639
//
3740
// NOTE: SecurityScopes are maps: no need to check uniqueness.
38-
func Spec(doc *loads.Document, formats strfmt.Registry) error {
39-
errs, _ /*warns*/ := NewSpecValidator(doc.Schema(), formats).Validate(doc)
41+
func Spec(doc *loads.Document, formats strfmt.Registry, options ...Option) error {
42+
errs, _ /*warns*/ := NewSpecValidator(doc.Schema(), formats, options...).Validate(doc)
4043
if errs.HasErrors() {
4144
return errors.CompositeValidationError(errs.Errors...)
4245
}
@@ -55,14 +58,20 @@ type SpecValidator struct {
5558
}
5659

5760
// NewSpecValidator creates a new swagger spec validator instance.
58-
func NewSpecValidator(schema *spec.Schema, formats strfmt.Registry) *SpecValidator {
59-
// schema options that apply to all called validators
61+
//
62+
// Options apply to the schema validators used internally. In particular, [WithPathLoader] injects
63+
// the document loader used to resolve $ref while validating the specification — set a confined
64+
// loader when validating a specification from an untrusted source (see the package "Security"
65+
// notes on [WithPathLoader]).
66+
func NewSpecValidator(schema *spec.Schema, formats strfmt.Registry, options ...Option) *SpecValidator {
67+
// schema options that apply to all called validators: built-in defaults first, then
68+
// caller-supplied options (which may add a loader or override a default).
6069
schemaOptions := new(SchemaValidatorOptions)
61-
for _, o := range []Option{
70+
for _, o := range append([]Option{
6271
SwaggerSchema(true),
6372
WithRecycleValidators(true),
6473
// withRecycleResults(true),
65-
} {
74+
}, options...) {
6675
o(schemaOptions)
6776
}
6877

@@ -252,7 +261,7 @@ func (s *SpecValidator) validateDuplicatePropertyNames() *Result {
252261

253262
func (s *SpecValidator) resolveRef(ref *spec.Ref) (*spec.Schema, error) {
254263
if s.spec.SpecFilePath() != "" {
255-
return spec.ResolveRefWithBase(s.spec.Spec(), ref, &spec.ExpandOptions{RelativeBase: s.spec.SpecFilePath()})
264+
return spec.ResolveRefWithBase(s.spec.Spec(), ref, s.schemaOptions.expandOptions(s.spec.SpecFilePath()))
256265
}
257266
// NOTE: it looks like with the new spec resolver, this code is now unrecheable
258267
return spec.ResolveRef(s.spec.Spec(), ref)
@@ -789,7 +798,10 @@ func (s *SpecValidator) validateReferencesValid() *Result {
789798
// NOTE: with default settings, loads.Document.Expanded()
790799
// stops on first error. Anyhow, the expand option to continue
791800
// on errors fails to report errors at all.
792-
exp, err := s.spec.Expanded()
801+
//
802+
// Pass the injected loader (if any) so whole-spec expansion is confined too. When no loader
803+
// is set, this is a no-op: loads falls back to the document's own loader.
804+
exp, err := s.spec.Expanded(s.schemaOptions.expandOptions(""))
793805
if err != nil {
794806
res.AddErrors(unresolvedReferencesMsg(err))
795807
}

0 commit comments

Comments
 (0)