Files
upstream-ory-kratos/schema/loader.go
upstream-mirror c60cc7ca75
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
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

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)
}