{"id":"GHSA-r9jg-fmfx-2gv6","kind":"vulnerability","aliases":["GHSA-r9jg-fmfx-2gv6"],"related":[],"title":"Possible cache poisoning in Action Pack content negotiation","description":"### Impact\nAction Pack negotiates the format of a rendered response from the `Accept`, `X-Requested-With` and, for XHR requests without `Accept`, `Content-Type` request headers, but `ActionController::Rendering#_set_vary_header` adds `Vary: Accept` only when the request carries a usable `Accept` header and the response has no `Vary` header yet. A response to a request without `Accept`, or to an XHR request, therefore omits the headers that selected its representation, and a response whose `Vary` the application set before rendering (for example `Vary: Accept-Language`) never gains `Accept`. A shared cache that honours `Vary` could store such a response and serve it to later clients that request a different representation, possibly leading to cache poisoning in which visitors receive, until the entry expires, a page without its layout or HTML in place of JSON. The attacker cannot choose the content of the response.\n\nApplications that render negotiated responses from actions marked `public` (for example with `expires_in ..., public: true`) behind a shared cache that honours `Vary`, such as Thruster or `Rack::Cache`, are affected.\n\n### Details\nIn the fixed releases, a rendered response whose format is not given by the URL lists `Accept` and `X-Requested-With` in `Vary`, and also `Content-Type` for an XHR request without `Accept`; when `config.action_dispatch.ignore_accept_header` is enabled, it lists only `X-Requested-With`. These fields are appended to an existing `Vary` value rather than skipped, and `Vary: *` is left unchanged. Shared caches can store more variants of such responses than before. See the Action Pack CHANGELOG for details.\n\n### Workarounds\nSet `Vary: Accept, X-Requested-With` on negotiated responses in the application, for example in an `after_action`, or do not mark negotiated responses `public`.\n\n### Releases\nThe fixed releases are available at the normal locations.\n\nPatches for those who cannot upgrade at once:\n- 8.1: https://github.com/rails/rails/commit/7a955ca36779938267c93902c47d1f55bab99b4b.patch\n- 8.0: https://github.com/rails/rails/commit/b58c107abe9c1f10c1d6a4941402c60e4dc4742c.patch\n\n### Credit\nThis issue was found by the Anthropic OSS Scanner (ANT-2026-7GHGLQGO, ANT-2026-5MT6C2R3).","summary":"Action Pack omits the Vary header that records which request headers chose a response's format, so a shared cache can store a response and serve it to clients asking for a different format. An attacker cannot choose the response content, but visitors may receive a page without its layout or HTML in place of JSON until the cache entry expires.","severity":"medium","cvss":4.8,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"actionpack","fixed":["8.1.4.1"],"affected":">= 8.1, < 8.1.4.1","ecosystem":"rubygems"},{"name":"actionpack","fixed":["8.0.5.2"],"affected":"< 8.0.5.2","ecosystem":"rubygems"}],"ecosystems":["rubygems"],"tags":["actionpack","cwe-436","package","rails","ruby","self-hosted"],"not_affected_if":["no-public-caching","no-shared-cache","single-format-responses"],"how_to_check":"- Run `bundle exec rails -v` or check `Gemfile.lock` for `actionpack` below `8.1.4.1` or below `8.0.5.2`.\n- Look for actions marked `public` (for example `expires_in ..., public: true`) that render negotiated responses behind a cache such as Thruster or `Rack::Cache`.\n- Check whether `config.action_dispatch.ignore_accept_header` is enabled.","fix":"- actionpack 8.1 series: move to `8.1.4.1`\n- actionpack 8.0 series: move to `8.0.5.2`","status":"open","published_at":"2026-11-04T03:02:08+00:00","first_seen_at":"2026-11-09T22:53:29+00:00","triaged_at":"2026-11-09T22:57:03+00:00","updated_at":"2026-11-09T22:57:03+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-11-09T22:57:03Z","model":"claude-haiku-5-5","output":{"fix":"- actionpack 8.1 series: move to `8.1.4.1`\n- actionpack 8.0 series: move to `8.0.5.2`","reach":"internet","title":"Rails Action Pack: missing Vary header can cache wrong response format","impact":"low","summary":"Action Pack omits the Vary header that records which request headers chose a response's format, so a shared cache can store a response and serve it to clients asking for a different format. An attacker cannot choose the response content, but visitors may receive a page without its layout or HTML in place of JSON until the cache entry expires.","audience":"developers","category":"package","nicknames":[],"who_cares":["rails","actionpack","ruby","self-hosted"],"confidence":0.8,"fix_status":"available","needs_auth":"no","worst_case":"shared cache serves wrong-format responses to other visitors until expiry","analyst_note":"","how_to_check":"- Run `bundle exec rails -v` or check `Gemfile.lock` for `actionpack` below `8.1.4.1` or below `8.0.5.2`.\n- Look for actions marked `public` (for example `expires_in ..., public: true`) that render negotiated responses behind a cache such as Thruster or `Rack::Cache`.\n- Check whether `config.action_dispatch.ignore_accept_header` is enabled.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["no-public-caching","no-shared-cache","single-format-responses"],"who_is_affected":"Applications that render format-negotiated responses (HTML or JSON from the same action) from actions marked public and served through a shared cache that honours Vary, such as Thruster or Rack::Cache. Applications without public caching of negotiated responses, or without a shared cache in front, are unlikely to be exposed.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"unknown"},"cost_usd":0.00087,"input_tokens":5375,"output_tokens":665,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-2026':418C,422C '-436':23B '-5':423C '-7':419C '/rails/rails/commit/7a955ca36779938267c93902c47d1f55bab99b4b.patch':402C '/rails/rails/commit/b58c107abe9c1f10c1d6a4941402c60e4dc4742c.patch':406C '2gv6':13A,18A '8.0':403C '8.1':399C 'accept':103C,113C,127C,135C,151C,179C,183C,287C,305C,308C,357C 'accept-languag':178C 'action':5A,30B,92C,245C,349C,373C 'actioncontrol':120C 'actionpack':19A,20A,21B 'add':125C 'affect':269C 'also':296C 'ant':417C,421C 'anthrop':414C 'append':322C 'applic':171C,239C,367C 'ask':59B 'attack':65B,231C 'avail':386C 'behind':255C 'cach':2A,49B,88B,186C,209C,258C,267C,337C 'cannot':66B,232C,395C 'carri':132C 'changelog':351C 'choos':67B,233C 'chose':41B 'client':58B,200C 'config.action_dispatch.ignore':307C 'content':7A,70B,115C,235C,298C 'content-typ':114C,297C 'could':190C 'credit':407C 'cwe':22B 'detail':270C,353C 'differ':62B,204C 'enabl':311C 'entri':89B,217C 'exampl':176C,249C,369C 'exist':325C 'expir':90B,218C,251C 'field':320C 'fix':273C,383C 'fmfx':12A,17A 'format':45B,63B,96C,279C 'found':411C 'gain':182C 'ghglqgo':420C 'ghsa':10A,15A 'ghsa-r9jg-fmfx-2gv6':9A,14A 'github.com':401C,405C 'github.com/rails/rails/commit/7a955ca36779938267c93902c47d1f55bab99b4b.patch':400C 'github.com/rails/rails/commit/b58c107abe9c1f10c1d6a4941402c60e4dc4742c.patch':404C 'given':282C 'header':35B,40B,118C,124C,136C,143C,160C,309C 'honour':188C,260C 'host':29B 'html':81B,225C 'impact':91C 'issu':409C 'json':85B,229C 'languag':180C 'later':199C 'layout':79B,223C 'lead':207C 'left':334C 'list':286C,313C 'locat':390C 'mark':246C,377C 'may':73B 'mt6c2r3':424C 'negoti':8A,94C,242C,363C,378C 'never':181C 'normal':389C 'omit':32B,158C 'oss':415C 'pack':6A,31B,93C,350C 'packag':24B 'page':76B,220C 'patch':391C 'place':83B,227C 'poison':3A,210C 'possibl':1A,206C 'public':247C,253C,380C 'r9jg':11A,16A 'rack':266C 'rail':25B 'rather':328C 'receiv':74B,214C 'record':37B 'releas':274C,381C,384C 'render':99C,121C,174C,241C,276C 'represent':164C,205C 'request':39B,106C,111C,117C,131C,149C,156C,202C,291C,303C,317C,360C 'respons':43B,53B,69B,100C,139C,146C,167C,194C,238C,243C,277C,344C,364C,379C 'rubi':26B 'scanner':416C 'see':347C 'select':162C 'self':28B 'self-host':27B 'serv':55B,196C 'set':122C,172C,355C 'share':48B,185C,257C,336C 'skip':330C 'store':51B,191C,339C 'therefor':157C 'thruster':264C 'true':254C 'type':116C,299C 'unchang':335C 'upgrad':396C 'url':285C 'usabl':134C 'valu':327C 'vari':34B,123C,126C,142C,169C,177C,189C,261C,294C,326C,332C,356C 'variant':341C 'visitor':72B,213C 'whose':168C,278C 'without':77B,112C,150C,221C,304C 'workaround':354C 'x':105C,290C,316C,359C 'x-requested-with':104C,289C,315C,358C 'xhr':110C,155C,302C 'yet':144C","nicknames":[],"mitigations":[],"who_affected":"Applications that render format-negotiated responses (HTML or JSON from the same action) from actions marked public and served through a shared cache that honours Vary, such as Thruster or Rack::Cache. Applications without public caching of negotiated responses, or without a shared cache in front, are unlikely to be exposed.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["actionpack"],"ptokens":["actionpack"],"labels":["actionpack","cwe436","medium","package","rails","ruby","rubygems","selfhosted"],"campaign_type":null,"search_a":"'2gv6':13A,18A 'action':5A 'actionpack':19A,20A 'cach':2A 'content':7A 'fmfx':12A,17A 'ghsa':10A,15A 'ghsa-r9jg-fmfx-2gv6':9A,14A 'negoti':8A 'pack':6A 'poison':3A 'possibl':1A 'r9jg':11A,16A","search_b":"'-436':23B 'action':30B 'actionpack':21B 'ask':59B 'attack':65B 'cach':49B,88B 'cannot':66B 'choos':67B 'chose':41B 'client':58B 'content':70B 'cwe':22B 'differ':62B 'entri':89B 'expir':90B 'format':45B,63B 'header':35B,40B 'host':29B 'html':81B 'json':85B 'layout':79B 'may':73B 'omit':32B 'pack':31B 'packag':24B 'page':76B 'place':83B 'rail':25B 'receiv':74B 'record':37B 'request':39B 'respons':43B,53B,69B 'rubi':26B 'self':28B 'self-host':27B 'serv':55B 'share':48B 'store':51B 'vari':34B 'visitor':72B 'without':77B","search_c":"'-2026':418C,422C '-5':423C '-7':419C '/rails/rails/commit/7a955ca36779938267c93902c47d1f55bab99b4b.patch':402C '/rails/rails/commit/b58c107abe9c1f10c1d6a4941402c60e4dc4742c.patch':406C '8.0':403C '8.1':399C 'accept':103C,113C,127C,135C,151C,179C,183C,287C,305C,308C,357C 'accept-languag':178C 'action':92C,245C,349C,373C 'actioncontrol':120C 'add':125C 'affect':269C 'also':296C 'ant':417C,421C 'anthrop':414C 'append':322C 'applic':171C,239C,367C 'attack':231C 'avail':386C 'behind':255C 'cach':186C,209C,258C,267C,337C 'cannot':232C,395C 'carri':132C 'changelog':351C 'choos':233C 'client':200C 'config.action_dispatch.ignore':307C 'content':115C,235C,298C 'content-typ':114C,297C 'could':190C 'credit':407C 'detail':270C,353C 'differ':204C 'enabl':311C 'entri':217C 'exampl':176C,249C,369C 'exist':325C 'expir':218C,251C 'field':320C 'fix':273C,383C 'format':96C,279C 'found':411C 'gain':182C 'ghglqgo':420C 'github.com':401C,405C 'github.com/rails/rails/commit/7a955ca36779938267c93902c47d1f55bab99b4b.patch':400C 'github.com/rails/rails/commit/b58c107abe9c1f10c1d6a4941402c60e4dc4742c.patch':404C 'given':282C 'header':118C,124C,136C,143C,160C,309C 'honour':188C,260C 'html':225C 'impact':91C 'issu':409C 'json':229C 'languag':180C 'later':199C 'layout':223C 'lead':207C 'left':334C 'list':286C,313C 'locat':390C 'mark':246C,377C 'mt6c2r3':424C 'negoti':94C,242C,363C,378C 'never':181C 'normal':389C 'omit':158C 'oss':415C 'pack':93C,350C 'page':220C 'patch':391C 'place':227C 'poison':210C 'possibl':206C 'public':247C,253C,380C 'rack':266C 'rather':328C 'receiv':214C 'releas':274C,381C,384C 'render':99C,121C,174C,241C,276C 'represent':164C,205C 'request':106C,111C,117C,131C,149C,156C,202C,291C,303C,317C,360C 'respons':100C,139C,146C,167C,194C,238C,243C,277C,344C,364C,379C 'scanner':416C 'see':347C 'select':162C 'serv':196C 'set':122C,172C,355C 'share':185C,257C,336C 'skip':330C 'store':191C,339C 'therefor':157C 'thruster':264C 'true':254C 'type':116C,299C 'unchang':335C 'upgrad':396C 'url':285C 'usabl':134C 'valu':327C 'vari':123C,126C,142C,169C,177C,189C,261C,294C,326C,332C,356C 'variant':341C 'visitor':213C 'whose':168C,278C 'without':112C,150C,221C,304C 'workaround':354C 'x':105C,290C,316C,359C 'x-requested-with':104C,289C,315C,358C 'xhr':110C,155C,302C 'yet':144C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-11-04T03:02:08+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-r9jg-fmfx-2gv6","url":"https://github.com/rails/rails/security/advisories/GHSA-r9jg-fmfx-2gv6","severity":"medium","published_at":"2026-11-04T03:02:08+00:00","modified_at":"2026-11-04T03:02:08+00:00","first_seen_at":"2026-11-09T22:53:29+00:00","fetched_at":"2026-11-09T22:53:29+00:00","note":null}],"documents":[{"id":110670,"url":"https://github.com/rails/rails/security/advisories/GHSA-r9jg-fmfx-2gv6","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}