open-feature / open-feature/go-sdk

[BUG] FlagMetadata is handed out by reference, so hooks and applications can mutate provider state

Open Beginner friendly
#556 0 comments 0 reactions 0 assignees View on GitHub
bug Needs Triage
Dominant language
Go
Stars
250
Forks
62
Avg merge
2d 3h
Merged PRs (30d)
25

Description

## Observed behavior

`ProviderResolutionDetail.ResolutionDetail()` assigns the provider's own map directly, with no copy:

https://github.com/open-feature/go-sdk/blob/6b0824c6c7b9b69e40f26aedcc4406ad4d3a5cec/openfeature/provider.go#L156-L169

That map flows into `EvaluationDetails.FlagMetadata` and is handed to every after/finally hook and to the application. `FlagMetadata` is a bare `map[string]any` with no protection:

https://github.com/open-feature/go-sdk/blob/6b0824c6c7b9b69e40f26aedcc4406ad4d3a5cec/openfeature/client.go#L156-L159

So a hook or the application can write straight into provider-owned state:

```
provider's own metadata map after the application mutated what it received:
map[injected-by:application owned-by:provider]
```

Two hazards follow. A hook or caller can corrupt provider state that later evaluations read. And because providers commonly return a cached or shared metadata map, concurrent evaluations can write the same map from multiple goroutines — an unsynchronized map write, which in Go is a `fatal error` rather than a recoverable panic.

## Expected Behavior

Per [1.4.15.1](https://openfeature.dev/specification/sections/flag-evaluation#conditional-requirement-14151): *"`Flag metadata` **MUST** be immutable."*

Copying the map on the way out would be the minimal fix. `FlagMetadata` already exposes `GetString`/`GetBool`/`GetInt`/`GetFloat` accessors, so a read-only shape is available without a breaking API change to consumers that use those.

For reference:

- **Java** — `ImmutableMetadata` with `asUnmodifiableMap()`.
- **JS** — `Object.freeze(resolution.flagMetadata ?? {})` at both construction sites.
- **Python** — plain dict, so Python has the same gap.

## Steps to reproduce

```go
// provider returns FlagMetadata{"owned-by": "provider"} — a map it retains as a field
d, _ := client.BooleanValueDetails(ctx, "f", false, of.EvaluationContext{})
d.FlagMetadata["injected-by"] = "application"

// the provider's own map now contains "injected-by"
```

Contributor guide

Open the contributing guide

Research direction

Start in openfeature/provider.go at ProviderResolutionDetail.ResolutionDetail(), then trace how the result reaches FlagMetadata in openfeature/client.go. Reproduce the issue with the retained provider map and add regression coverage showing that mutations to returned metadata do not affect provider state; the concurrent shared-map hazard should also be addressed.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
api
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.