open-feature / open-feature/go-sdk
[BUG] FlagMetadata is handed out by reference, so hooks and applications can mutate provider state
- 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
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