Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
109 lines
3.6 KiB
Go
109 lines
3.6 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package schema
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
|
|
_ "github.com/ory/jsonschema/v3/base64loader"
|
|
_ "github.com/ory/jsonschema/v3/fileloader"
|
|
"github.com/ory/jsonschema/v3/httploader"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/ory/jsonschema/v3"
|
|
"github.com/ory/x/httpx"
|
|
)
|
|
|
|
// loadRefURL resolves the URL of a `$ref` inside a schema. It enforces the
|
|
// scheme allowlist and then delegates to the jsonschema package's global
|
|
// loader table. The global `file` loader remains registered so that
|
|
// operator-configured top-level schema URLs (resolved outside the compiler)
|
|
// keep working.
|
|
func loadRefURL(ctx context.Context, raw string) (io.ReadCloser, error) {
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid $ref URL %q: %w", raw, err)
|
|
}
|
|
if u.Scheme != "base64" {
|
|
return nil, fmt.Errorf("$ref scheme %q is not permitted in identity schemas", u.Scheme)
|
|
}
|
|
return jsonschema.LoadURL(ctx, raw)
|
|
}
|
|
|
|
// NewCompiler returns a jsonschema.Compiler. When disallowRefs is true, the
|
|
// compiler rejects `file://` URLs (and any other non-allowlisted scheme) in
|
|
// `$ref` values, preventing an attacker-supplied schema from reading local
|
|
// files on the Kratos host. When disallowRefs is false, the compiler uses
|
|
// the jsonschema library's default loader table, which preserves legacy
|
|
// behavior for operators who intentionally reference local files.
|
|
//
|
|
// The flag is controlled by `security.disallow_ref_in_identity_schemas`.
|
|
// Ory Network forces it on.
|
|
func NewCompiler(disallowRefs bool) *jsonschema.Compiler {
|
|
c := jsonschema.NewCompiler()
|
|
if disallowRefs {
|
|
c.LoadURL = loadRefURL
|
|
}
|
|
return c
|
|
}
|
|
|
|
// NewCompilerWithURL returns a NewCompiler with the top-level schema at
|
|
// schemaURL already loaded and registered via the trusted global loader
|
|
// (which always supports `file://` for operator-configured top-level URLs,
|
|
// regardless of disallowRefs). See NewCompiler for the semantics of
|
|
// disallowRefs.
|
|
func NewCompilerWithURL(ctx context.Context, schemaURL string, disallowRefs bool) (*jsonschema.Compiler, error) {
|
|
ctx = ensureGuardedHTTPClient(ctx)
|
|
|
|
resource, err := jsonschema.LoadURL(ctx, schemaURL)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
defer func() { _ = resource.Close() }()
|
|
|
|
// Read the body with a hard size cap so a malicious schema URL cannot
|
|
// OOM kratos by returning a multi-GB body.
|
|
data, err := io.ReadAll(io.LimitReader(resource, MaxSchemaBodyBytes+1))
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
if len(data) > MaxSchemaBodyBytes {
|
|
return nil, errors.Errorf("identity schema rejected: body exceeds %d bytes", MaxSchemaBodyBytes)
|
|
}
|
|
|
|
// Decode once for structural prevalidation. The upstream compiler will
|
|
// decode again from the same bytes — accepting a small CPU duplication
|
|
// in exchange for the security gate.
|
|
var doc any
|
|
if err := json.Unmarshal(data, &doc); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
if err := preValidateSchema(doc); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
c := NewCompiler(disallowRefs)
|
|
if err := c.AddResource(schemaURL, bytes.NewReader(data)); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
var defaultHTTPClient = httpx.NewResilientClient(httpx.ResilientClientDisallowInternalIPs())
|
|
|
|
// ensureGuardedHTTPClient guarantees that the jsonschema httploader will
|
|
// pick up a client whose dialer rejects internal IP ranges.
|
|
func ensureGuardedHTTPClient(ctx context.Context) context.Context {
|
|
if ctx.Value(httploader.ContextKey) != nil {
|
|
return ctx
|
|
}
|
|
return context.WithValue(ctx, httploader.ContextKey, defaultHTTPClient)
|
|
}
|