prevent SSRF and local file read via external $ref in values.schema.json

Signed-off-by: SABITHSAHEB <shabi7204192361@gmail.com>
pull/32245/head
SABITHSAHEB 3 weeks ago
parent be45213874
commit 6b2b4be6b0

@ -136,11 +136,14 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reter
}
slog.Debug("unmarshalled JSON schema", "schema", schemaJSON)
// Configure compiler with loaders for different URL schemes
// Refuse external schema references (http(s):// and file://) instead of
// fetching them; see denyURLLoader. The urn scheme stays resolvable via the
// pluggable URNResolver for back-compatibility.
deny := denyURLLoader{}
loader := jsonschema.SchemeURLLoader{
"file": jsonschema.FileLoader{},
"http": newHTTPURLLoader(),
"https": newHTTPURLLoader(),
"file": deny,
"http": deny,
"https": deny,
"urn": urnLoader{},
}
@ -164,6 +167,28 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reter
return nil
}
// denyURLLoader is a [jsonschema.URLLoader] that refuses to resolve any
// external schema reference.
//
// A chart's values.schema.json is untrusted input (loaded verbatim from a chart
// archive that may originate from a remote repository or OCI registry). If the
// JSON Schema compiler is allowed to follow external "$ref"/"$id"/"$schema"
// references, a malicious chart can drive Helm into fetching attacker-chosen
// URLs while validating values: "http(s)://" references become a server-side
// request forgery primitive (e.g. against cloud-metadata endpoints) and
// "file://" references read arbitrary local files into the schema graph - the
// JSON Schema analogue of an XXE attack.
//
// Standard meta-schemas (the json-schema.org drafts named by "$schema") are
// served by the compiler from an embedded copy and never reach a loader, so
// failing closed here keeps schema validation a purely local, in-archive
// operation without breaking legitimate charts.
type denyURLLoader struct{}
func (denyURLLoader) Load(url string) (any, error) {
return nil, fmt.Errorf("loading external schema reference %q is not allowed", url)
}
// URNResolverFunc allows SDK to plug a URN resolver. It must return a
// schema document compatible with the validator (e.g., result of
// jsonschema.UnmarshalJSON).

@ -17,10 +17,13 @@ limitations under the License.
package util
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"helm.sh/helm/v4/pkg/chart/common"
@ -300,6 +303,46 @@ func TestValidateAgainstSingleSchema_UnresolvedURN_Ignored(t *testing.T) {
}
}
// TestValidateAgainstSingleSchema_ExternalRefDenied ensures that external schema
// references in an (untrusted) chart schema are refused rather than resolved,
// closing the SSRF / arbitrary-local-file-read vector. See denyURLLoader.
func TestValidateAgainstSingleSchema_ExternalRefDenied(t *testing.T) {
// An http:// $ref must fail closed and must not trigger an outbound request.
t.Run("http ref is not fetched (SSRF)", func(t *testing.T) {
var hits int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
atomic.AddInt32(&hits, 1)
w.Write([]byte(`{"type": "object"}`))
}))
defer server.Close()
schema := []byte(fmt.Sprintf(`{"$ref": %q}`, server.URL+"/evil.json"))
err := ValidateAgainstSingleSchema(common.Values{"any": "value"}, schema)
if err == nil {
t.Fatal("expected validation to fail closed on an external http $ref")
}
if got := atomic.LoadInt32(&hits); got != 0 {
t.Fatalf("external $ref was fetched: server was contacted %d time(s)", got)
}
})
// A file:// $ref must fail closed and must not read local file contents.
t.Run("file ref is not read", func(t *testing.T) {
secret := filepath.Join(t.TempDir(), "secret.txt")
if err := os.WriteFile(secret, []byte("TOP-SECRET"), 0o600); err != nil {
t.Fatal(err)
}
schema := []byte(fmt.Sprintf(`{"$ref": %q}`, "file:///"+filepath.ToSlash(secret)))
err := ValidateAgainstSingleSchema(common.Values{"any": "value"}, schema)
if err == nil {
t.Fatal("expected validation to fail closed on an external file $ref")
}
if strings.Contains(err.Error(), "TOP-SECRET") {
t.Fatalf("local file content leaked through schema validation: %v", err)
}
})
}
// Non-regression tests for https://github.com/helm/helm/issues/31202
// Ensure ValidateAgainstSchema does not panic when:
// - subchart key is missing

Loading…
Cancel
Save