fix: prevent Resource.response() from mutating handler-owned attributes#202
Conversation
Resource.response() previously assigned `response := r.Attributes`, a
reference copy of the handler's stored map. Subsequent writes of
framework-injected keys ("id", "schemas", "meta") therefore propagated back
into the handler's own storage. A second GET on the same resource would
return those keys as if they were user-supplied attributes.
Replace the reference assignment with an explicit copy so the framework
operates only on a local map.
Adds a regression test that performs a GET request and then inspects the
handler's internal store to assert that none of the framework keys were
written into it.
|
Thanks for tracking this down! The diagnosis and the test are great, and the fact that a second While reviewing I noticed the same aliasing pattern lives in one other place: Outer map, same shape as the one you fixed: attrs := v.Attributes // reference copy
if attrs == nil {
attrs = ResourceAttributes{}
}
attrs[schema.CommonAttributeID] = v.ID
// ...Nested meta map, sneakier; if the if existing, ok := attrs[schema.CommonAttributeMeta]; ok {
if m, ok := existing.(map[string]interface{}); ok {
metaMap = m // alias of caller's nested map
}
}
// ...
metaMap["created"] = ... // mutates itSame fix shape as your PR works: copy the outer map up front, and copy the nested meta map before merging: attrs := make(ResourceAttributes, len(v.Attributes)+3)
for k, val := range v.Attributes {
attrs[k] = val
}
// ...
if existing, ok := attrs[schema.CommonAttributeMeta]; ok {
if m, ok := existing.(map[string]interface{}); ok {
metaMap = make(map[string]interface{}, len(m)+3)
for k, val := range m {
metaMap[k] = val
}
}
}Would you be willing to include a fix for this in the PR too @eslerm? If not, happy to merge this and do it later. NOTE: I have not tested the code above yet, so please validated it 🙏🏼 |
|
One thought on scope: the most obvious reproducer needs a long-lived in-memory map (like the repo's own Out of curiosity, how did you find this? Real incident, code read, or tooling? Useful to know whether there's a class of similar bugs worth sweeping in one pass. |
The same aliasing bug found in Resource.response() also exists in rawResources(): the outer attrs map and any handler-provided nested meta map were both reference-copied, so injecting id/externalId/meta fields corrupted the handler's stored state on subsequent calls. Fix both layers with explicit copies. Adds regression tests for both the existing Resource.response() fix and the new rawResources() fix.
|
Cheers, added. I upgraded to Go 1.26 and ran This analysis was loose, but you may want to investigate b34c00f |
Problem
Resource.response()opened with:This is a reference copy, not a value copy. Every subsequent write to
response(injecting"id","schemas","meta") therefore propagated back into the map owned by the resource handler.The consequence is silent corruption of handler state: a second GET on the same resource would find those framework-injected keys already present in the attributes map and return them as if they were user-supplied data.
Fix
Replace the reference assignment with an explicit copy:
Test
TestResponseDoesNotMutateHandlerAttributesperforms a GET request, then inspects the handler's internal store directly and asserts that none of"id","schemas", or"meta"were written into it. Without the fix the test fails on all three keys.