VYPR
High severityNVD Advisory· Published May 1, 2025· Updated Apr 15, 2026

CVE-2025-46569

CVE-2025-46569

Description

Open Policy Agent (OPA) is an open source, general-purpose policy engine. Prior to version 1.4.0, when run as a server, OPA exposes an HTTP Data API for reading and writing documents. Requesting a virtual document through the Data API entails policy evaluation, where a Rego query containing a single data document reference is constructed from the requested path. This query is then used for policy evaluation. A HTTP request path can be crafted in a way that injects Rego code into the constructed query. The evaluation result cannot be made to return any other data than what is generated by the requested path, but this path can be misdirected, and the injected Rego code can be crafted to make the query succeed or fail; opening up for oracle attacks or, given the right circumstances, erroneous policy decision results. Furthermore, the injected code can be crafted to be computationally expensive, resulting in a Denial Of Service (DoS) attack. This issue has been patched in version 1.4.0. A workaround involves having network access to OPA’s RESTful APIs being limited to localhost and/or trusted networks, unless necessary for production reasons.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/open-policy-agent/opa/v1/serverGo
< 1.4.01.4.0
github.com/open-policy-agent/opa/serverGo
< 1.4.01.4.0
github.com/open-policy-agent/opaGo
< 1.4.01.4.0

Patches

2
ad2063247a14

Merge commit from fork

https://github.com/open-policy-agent/opaJohan FyllingMay 1, 2025via ghsa
3 files changed · +226 17
  • v1/server/server.go+77 15 modified
    @@ -1148,19 +1148,23 @@ func (s *Server) v0QueryPath(w http.ResponseWriter, r *http.Request, urlPath str
     	}
     
     	if len(rs) == 0 {
    -		ref := stringPathToDataRef(urlPath)
    +		ref, err := stringPathToDataRef(urlPath)
    +		if err != nil {
    +			writer.Error(w, http.StatusBadRequest, types.NewErrorV1(types.CodeInvalidParameter, "invalid path: %v", err))
    +			return
    +		}
     
     		var messageType = types.MsgMissingError
     		if len(s.getCompiler().GetRulesForVirtualDocument(ref)) > 0 {
     			messageType = types.MsgFoundUndefinedError
     		}
    -		err := types.NewErrorV1(types.CodeUndefinedDocument, "%v: %v", messageType, ref)
    -		if err := logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m); err != nil {
    +		errV1 := types.NewErrorV1(types.CodeUndefinedDocument, "%v: %v", messageType, ref)
    +		if err := logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, errV1, m); err != nil {
     			writer.ErrorAuto(w, err)
     			return
     		}
     
    -		writer.Error(w, http.StatusNotFound, err)
    +		writer.Error(w, http.StatusNotFound, errV1)
     		return
     	}
     	err = logger.Log(ctx, txn, urlPath, "", goInput, input, &rs[0].Expressions[0].Value, ndbCache, nil, m)
    @@ -1319,10 +1323,15 @@ func (s *Server) unversionedGetHealthWithPolicy(w http.ResponseWriter, r *http.R
     	vars := mux.Vars(r)
     	urlPath := vars["path"]
     	healthDataPath := "/system/health/" + urlPath
    -	healthDataPath = stringPathToDataRef(healthDataPath).String()
    +
    +	healthDataPathQuery, err := stringPathToQuery(healthDataPath)
    +	if err != nil {
    +		writer.Error(w, http.StatusBadRequest, types.NewErrorV1(types.CodeInvalidParameter, "invalid path: %v", err))
    +		return
    +	}
     
     	rego := rego.New(
    -		rego.Query(healthDataPath),
    +		rego.ParsedQuery(healthDataPathQuery),
     		rego.Compiler(s.getCompiler()),
     		rego.Store(s.store),
     		rego.Input(input),
    @@ -1337,7 +1346,7 @@ func (s *Server) unversionedGetHealthWithPolicy(w http.ResponseWriter, r *http.R
     	}
     
     	if len(rs) == 0 {
    -		writeHealthResponse(w, fmt.Errorf("health check (%v) was undefined", healthDataPath))
    +		writeHealthResponse(w, fmt.Errorf("health check (%v) was undefined", healthDataPathQuery))
     		return
     	}
     
    @@ -1347,7 +1356,7 @@ func (s *Server) unversionedGetHealthWithPolicy(w http.ResponseWriter, r *http.R
     		return
     	}
     
    -	writeHealthResponse(w, fmt.Errorf("health check (%v) returned unexpected value", healthDataPath))
    +	writeHealthResponse(w, fmt.Errorf("health check (%v) returned unexpected value", healthDataPathQuery))
     }
     
     func writeHealthResponse(w http.ResponseWriter, err error) {
    @@ -2549,12 +2558,15 @@ func (s *Server) makeRego(_ context.Context,
     	tracer topdown.QueryTracer,
     	opts []func(*rego.Rego),
     ) (*rego.Rego, error) {
    -	queryPath := stringPathToDataRef(urlPath).String()
    +	query, err := stringPathToQuery(urlPath)
    +	if err != nil {
    +		return nil, types.NewErrorV1(types.CodeInvalidParameter, "invalid path: %v", err)
    +	}
     
     	opts = append(
     		opts,
     		rego.Transaction(txn),
    -		rego.Query(queryPath),
    +		rego.ParsedQuery(query),
     		rego.ParsedInput(input),
     		rego.Metrics(m),
     		rego.QueryTracer(tracer),
    @@ -2569,6 +2581,43 @@ func (s *Server) makeRego(_ context.Context,
     	return rego.New(opts...), nil
     }
     
    +func stringPathToQuery(urlPath string) (ast.Body, error) {
    +	ref, err := stringPathToDataRef(urlPath)
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	return parseRefQuery(ref.String())
    +}
    +
    +// parseRefQuery parses a string into a query ast.Body.
    +// The resulting query must be comprised of a single ref, or an error will be returned.
    +func parseRefQuery(str string) (ast.Body, error) {
    +	query, err := ast.ParseBody(str)
    +	if err != nil {
    +		return nil, errors.New("failed to parse query")
    +	}
    +
    +	// assert the query is exactly one statement
    +	if l := len(query); l == 0 {
    +		return nil, errors.New("no ref")
    +	} else if l > 1 {
    +		return nil, errors.New("complex query")
    +	}
    +
    +	// assert the single statement is a lone ref
    +	expr := query[0]
    +	switch t := expr.Terms.(type) {
    +	case *ast.Term:
    +		switch t.Value.(type) {
    +		case ast.Ref:
    +			return query, nil
    +		}
    +	}
    +
    +	return nil, errors.New("complex query")
    +}
    +
     func (*Server) prepareV1PatchSlice(root string, ops []types.PatchV1) (result []patchImpl, err error) {
     
     	root = "/" + strings.Trim(root, "/")
    @@ -2677,31 +2726,44 @@ func (s *Server) updateNDCache(enabled bool) {
     	s.ndbCacheEnabled = enabled
     }
     
    -func stringPathToDataRef(s string) (r ast.Ref) {
    +func stringPathToDataRef(s string) (ast.Ref, error) {
     	result := ast.Ref{ast.DefaultRootDocument}
    -	return append(result, stringPathToRef(s)...)
    +	r, err := stringPathToRef(s)
    +	if err != nil {
    +		return nil, err
    +	}
    +	return append(result, r...), nil
     }
     
    -func stringPathToRef(s string) (r ast.Ref) {
    +func stringPathToRef(s string) (ast.Ref, error) {
    +	r := ast.Ref{}
    +
     	if len(s) == 0 {
    -		return r
    +		return r, nil
     	}
    +
     	p := strings.Split(s, "/")
     	for _, x := range p {
     		if x == "" {
     			continue
     		}
    +
     		if y, err := url.PathUnescape(x); err == nil {
     			x = y
     		}
    +
    +		if strings.Contains(x, "\"") {
    +			return nil, fmt.Errorf("invalid ref term '%s'", x)
    +		}
    +
     		i, err := strconv.Atoi(x)
     		if err != nil {
     			r = append(r, ast.StringTerm(x))
     		} else {
     			r = append(r, ast.IntNumberTerm(i))
     		}
     	}
    -	return r
    +	return r, nil
     }
     
     func validateQuery(query string, opts ast.ParserOptions) (ast.Body, error) {
    
  • v1/server/server_test.go+149 1 modified
    @@ -3383,7 +3383,6 @@ func TestDataMetricsEval(t *testing.T) {
     			"counter_disk_read_keys",
     			"counter_disk_read_bytes",
     			"timer_rego_input_parse_ns",
    -			"timer_rego_query_parse_ns",
     			"timer_rego_query_compile_ns",
     			"timer_rego_query_eval_ns",
     			"timer_server_handler_ns",
    @@ -6811,3 +6810,152 @@ func zipString(input string) []byte {
     	}
     	return b.Bytes()
     }
    +
    +func TestStringPathToDataRef(t *testing.T) {
    +	t.Parallel()
    +
    +	cases := []struct {
    +		note   string
    +		path   string
    +		expRef string
    +		expErr string
    +	}{
    +		{path: "foo", expRef: `data.foo`},
    +		{path: "foo/", expRef: `data.foo`},
    +		{path: "foo/bar", expRef: `data.foo.bar`},
    +		{path: "foo/bar/", expRef: `data.foo.bar`},
    +		{path: "foo/../bar", expRef: `data.foo[".."].bar`},
    +
    +		// Path injection attack
    +		// url path: `foo%22%5D%3Bmalicious_call%28%29%3Bx%3D%5B%22`
    +		// url decoded: `foo"];malicious_call();x=["`
    +		// data ref .String(): `data.foo["\"];malicious_call();x=[\""]`
    +		// Above attack is mitigated by rejecting any ref component containing string terminators (`"`).
    +		{
    +			note:   "string terminals inside ref term",
    +			path:   "foo%22%5D%3Bmalicious_call%28%29%3Bx%3D%5B%22", // foo"];malicious_call();x=["
    +			expErr: `invalid ref term 'foo"];malicious_call();x=["'`,
    +		},
    +	}
    +
    +	for _, tc := range cases {
    +		note := tc.note
    +		if note == "" {
    +			note = strings.ReplaceAll(tc.path, "/", "_")
    +		}
    +
    +		t.Run(note, func(t *testing.T) {
    +			ref, err := stringPathToDataRef(tc.path)
    +
    +			if tc.expRef != "" {
    +				if err != nil {
    +					t.Fatalf("Expected ref:\n\n%s\n\nbut got error:\n\n%s", tc.expRef, err)
    +				}
    +				if refStr := ref.String(); refStr != tc.expRef {
    +					t.Fatalf("Expected ref:\n\n%s\n\nbut got:\n\n%s", tc.expRef, refStr)
    +				}
    +			}
    +
    +			if tc.expErr != "" {
    +				if ref != nil {
    +					t.Fatalf("Expected error:\n\n%s\n\nbut got ref:\n\n%s", tc.expErr, ref.String())
    +				}
    +				if errStr := err.Error(); errStr != tc.expErr {
    +					t.Fatalf("Expected error:\n\n%s\n\nbut got ref:\n\n%s", tc.expErr, errStr)
    +				}
    +			}
    +		})
    +	}
    +}
    +
    +func TestParseRefQuery(t *testing.T) {
    +	t.Parallel()
    +
    +	cases := []struct {
    +		note    string
    +		raw     string
    +		expBody ast.Body
    +		expErr  string
    +	}{
    +		{
    +			note:   "unparseable",
    +			raw:    `}abc{`,
    +			expErr: "failed to parse query",
    +		},
    +		{
    +			note:   "empty",
    +			raw:    ``,
    +			expErr: "no ref",
    +		},
    +		{
    +			note:    "single ref",
    +			raw:     `data.foo.bar`,
    +			expBody: ast.MustParseBody(`data.foo.bar`),
    +		},
    +		{
    +			note:   "multiple refs,';' separated",
    +			raw:    `data.foo.bar;data.baz.qux`,
    +			expErr: "complex query",
    +		},
    +		{
    +			note: "multiple refs,newline separated",
    +			raw: `data.foo.bar
    +data.baz.qux`,
    +			expErr: "complex query",
    +		},
    +		{
    +			note:   "single ref + call",
    +			raw:    `data.foo.bar;data.baz.qux()`,
    +			expErr: "complex query",
    +		},
    +		{
    +			note:   "single ref + assignment",
    +			raw:    `data.foo.bar;x := 42`,
    +			expErr: "complex query",
    +		},
    +		{
    +			note:   "single call",
    +			raw:    `data.foo.bar()`,
    +			expErr: "complex query",
    +		},
    +		{
    +			note:   "single assignment",
    +			raw:    `x := 42`,
    +			expErr: "complex query",
    +		},
    +		{
    +			note:   "single unification",
    +			raw:    `x = 42`,
    +			expErr: "complex query",
    +		},
    +		{
    +			note:   "single equality",
    +			raw:    `x == 42`,
    +			expErr: "complex query",
    +		},
    +	}
    +
    +	for _, tc := range cases {
    +		t.Run(tc.note, func(t *testing.T) {
    +			body, err := parseRefQuery(tc.raw)
    +
    +			if tc.expBody != nil {
    +				if err != nil {
    +					t.Fatalf("Expected body:\n\n%s\n\nbut got error:\n\n%s", tc.expBody, err)
    +				}
    +				if body.String() != tc.expBody.String() {
    +					t.Fatalf("Expected body:\n\n%s\n\nbut got:\n\n%s", tc.expBody, body.String())
    +				}
    +			}
    +
    +			if tc.expErr != "" {
    +				if body != nil {
    +					t.Fatalf("Expected error:\n\n%s\n\nbut got body:\n\n%s", tc.expErr, body.String())
    +				}
    +				if errStr := err.Error(); errStr != tc.expErr {
    +					t.Fatalf("Expected error:\n\n%s\n\nbut got body:\n\n%s", tc.expErr, errStr)
    +				}
    +			}
    +		})
    +	}
    +}
    
  • v1/test/e2e/metrics/metrics_test.go+0 1 modified
    @@ -212,7 +212,6 @@ func assertDataInstrumentationMetricsInMap(t *testing.T, includeCompile bool, me
     		"timer_server_handler_ns",
     	}
     	compileStageKeys := []string{
    -		"timer_rego_query_parse_ns",
     		"timer_rego_query_compile_ns",
     		"timer_query_compile_stage_build_comprehension_index_ns",
     		"timer_query_compile_stage_check_safety_ns",
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.