CVE-2026-54659: Pagy Pagy I18n Locale Option Is Vulnerability

MODERATE
Advisory Identifier: GHSA-2xmw-f8j8-wfxc
CWE-200 CWE-22
Published
Advisory Summary

CVE-2026-54659 is a MODERATE pagy i18n locale option is vulnerability (CWE-200: Exposure of Sensitive Information to an Unauthorized Actor) affecting Pagy libraries in the RubyGems ecosystem. The issue involves pagy i18n locale option is not validated before being used in a file path. Remediation is available by upgrading affected packages to version 43.5.6.

CVE-2026-54659 at a Glance

CVE Identifier CVE-2026-54659
GHSA Identifier GHSA-2xmw-f8j8-wfxc
Severity MODERATE
CVSS v3.1 Base Score N/A
CWE Classification CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
Affected Product Pagy
Package Ecosystem RubyGems
Published Date
Last Updated

How to Fix CVE-2026-54659

To remediate this vulnerability, upgrade all affected Pagy dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

pagy fixed: 43.5.6
Upgrade pagy to 43.5.6
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for pagy

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-54659, inspect your project dependencies for Pagy:

inspect dependencies for pagy

Analyze the terminal output to locate any version of Pagy in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 43.5.6), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-200: Exposure of Sensitive Information to an Unauthorized Actor, where the product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. In Pagy, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary `Pagy::I18n.locale=` did not validate its argument before using it as a path component to load the matching dictionary file (`<locale>.yml`). An application that assigns untrusted input to the locale — e.g. the common pattern `Pagy::I18n.locale = params[:locale]` — let that input influence which file Pagy attempted to load. ### Details The setter stored the value as-is, and the loader joined it into a path and read it: ```ruby # gem/lib/pagy/modules/i18n/i18n.rb def locale=(value) Thread.current[:pagy_locale] = value.to_s end # ...later, when translating: path = pathnames.reverse.map { |p| p.join("#{locale}.yml") }.find(&:exist?) dictionary = YAML.load_file(path)[locale] ``` Because the locale was used verbatim, a value such as an absolute path or a `../`-style string redirected the lookup outside the locales directory. Pagy's subsequent structural check (`dictionary['pagy']['p11n']`) prevents the file's contents from being returned, so this is **not** a direct file read. Fixed in 43.5.6 by constraining the locale to a BCP 47 shape before use: ```ruby LOCALE_PATTERN = /\A[a-zA-Z]{2,8}(-[a-zA-Z0-9]{1,8})*\z/ def locale=(value) Thread.current[:pagy_locale] = value.to_s[LOCALE_PATTERN] end ``` Any non-matching value (including `nil`) resolves to the default locale and never reaches the file lookup. ### PoC In an application that sets `Pagy::I18n.locale = params[:locale]`, the loader appends `.yml` and reads `<locale>.yml`, so the request param controls the target path. For example, pointing it at the app's `config/database.yml`: 1. Send a request with `?locale=../../../config/database` (adjust the number of `../` to reach the app root from the gem's `locales/` directory). 2. Pagy calls `YAML.load_file` on the resulting `…/config/database.yml`. 3. The outcome differs by whether that `.yml` exists, is readable, parses as YAML, and has Pagy's expected structure — an existing, readable `config/database.yml` raises a different error than a non-existent path (which silently falls back to the default locale). This yields a file-existence / readability oracle for `.yml` paths, and the targeted file is read into the process during the attempt. ### Impact Information disclosure (CWE-22 / CWE-200): a file-existence / readability oracle for `.yml` paths on the host, plus a server-side read of attacker-chosen files into the process. The file contents are not returned in the response. Only applications that pass **unsanitized end-user input** into `Pagy::I18n.locale=` are affected. Applications that set the locale from trusted values are not affected. **Patched:** pagy 43.5.6. **Workaround (if you cannot upgrade):** validate the locale before assigning it, e.g. `Pagy::I18n.locale = params[:locale].to_s[/\A[a-zA-Z]{2,8}(-[a-zA-Z0-9]{1,8})*\z/]`, or restrict it to your known set of locales.

CVE-2026-54659 Affected Pagy Versions

1 component(s)
Fixed Versions:
43.5.6
Product / Package Ecosystem Affected Versions Fixed Version
pagy
RubyGems
≥ 43.0.0
< 43.5.6
43.5.6

References

Frequently Asked Questions

What is CVE-2026-54659?
CVE-2026-54659 is a security vulnerability in Pagy involving Pagy I18n locale option is not validated before being used in a file path under CWE-200: Exposure of Sensitive Information to an Unauthorized Actor.
Which Pagy versions are affected?
Affected versions include releases prior to 43.5.6 across affected packages: pagy.
What version fixes CVE-2026-54659?
Upgrading to 43.5.6 (or the latest patched release for your respective branch) resolves the vulnerability.
Is pagy affected?
Yes, pagy is affected in releases prior to 43.5.6. Upgrade your dependency to 43.5.6 or later.
What CWE is associated with CVE-2026-54659?
CVE-2026-54659 is classified under CWE-200: Exposure of Sensitive Information to an Unauthorized Actor (The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.).
How can I check RubyGems dependencies for CVE-2026-54659?
Run the dependency tree command: 'inspect dependencies for pagy'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-2xmw-f8j8-wfxc",
  "modified" : "2026-07-28T22:25:29Z",
  "published" : "2026-07-28T22:25:29Z",
  "aliases" : [ "CVE-2026-54659" ],
  "summary" : "Pagy I18n locale option is not validated before being used in a file path",
  "details" : "### Summary\n\n`Pagy::I18n.locale=` did not validate its argument before using it as a\npath component to load the matching dictionary file (`<locale>.yml`). An\napplication that assigns untrusted input to the locale — e.g. the common\npattern `Pagy::I18n.locale = params[:locale]` — let that input influence\nwhich file Pagy attempted to load.\n\n### Details\n\nThe setter stored the value as-is, and the loader joined it into a path\nand read it:\n\n```ruby\n# gem/lib/pagy/modules/i18n/i18n.rb\ndef locale=(value)\n  Thread.current[:pagy_locale] = value.to_s\nend\n\n# ...later, when translating:\npath = pathnames.reverse.map { |p| p.join(\"#{locale}.yml\") }.find(&:exist?)\ndictionary = YAML.load_file(path)[locale]\n```\n\nBecause the locale was used verbatim, a value such as an absolute path or\na `../`-style string redirected the lookup outside the locales directory.\nPagy's subsequent structural check (`dictionary['pagy']['p11n']`)\nprevents the file's contents from being returned, so this is **not** a\ndirect file read.\n\nFixed in 43.5.6 by constraining the locale to a BCP 47 shape before use:\n\n```ruby\nLOCALE_PATTERN = /\\A[a-zA-Z]{2,8}(-[a-zA-Z0-9]{1,8})*\\z/\n\ndef locale=(value)\n  Thread.current[:pagy_locale] = value.to_s[LOCALE_PATTERN]\nend\n```\n\nAny non-matching value (including `nil`) resolves to the default locale\nand never reaches the file lookup.\n\n### PoC\n\nIn an application that sets `Pagy::I18n.locale = params[:locale]`, the\nloader appends `.yml` and reads `<locale>.yml`, so the request param\ncontrols the target path. For example, pointing it at the app's\n`config/database.yml`:\n\n1. Send a request with `?locale=../../../config/database` (adjust the\n   number of `../` to reach the app root from the gem's `locales/`\n   directory).\n2. Pagy calls `YAML.load_file` on the resulting `…/config/database.yml`.\n3. The outcome differs by whether that `.yml` exists, is readable, parses\n   as YAML, and has Pagy's expected structure — an existing, readable\n   `config/database.yml` raises a different error than a non-existent\n   path (which silently falls back to the default locale). This yields a\n   file-existence / readability oracle for `.yml` paths, and the targeted\n   file is read into the process during the attempt.\n\n### Impact\n\nInformation disclosure (CWE-22 / CWE-200): a file-existence / readability\noracle for `.yml` paths on the host, plus a server-side read of\nattacker-chosen files into the process. The file contents are not\nreturned in the response.\n\nOnly applications that pass **unsanitized end-user input** into\n`Pagy::I18n.locale=` are affected. Applications that set the locale from\ntrusted values are not affected.\n\n**Patched:** pagy 43.5.6.\n**Workaround (if you cannot upgrade):** validate the locale before\nassigning it, e.g.\n`Pagy::I18n.locale = params[:locale].to_s[/\\A[a-zA-Z]{2,8}(-[a-zA-Z0-9]{1,8})*\\z/]`,\nor restrict it to your known set of locales.",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "RubyGems",
      "name" : "pagy"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "43.0.0"
      }, {
        "fixed" : "43.5.6"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/ddnexus/pagy/security/advisories/GHSA-2xmw-f8j8-wfxc"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/ddnexus/pagy/pull/908"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/ddnexus/pagy/commit/efcf09690e9fa7d7abdfb987b785a55f87e287df"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/ddnexus/pagy"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/ddnexus/pagy/releases/tag/43.5.6"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-200", "CWE-22" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-28T22:25:29Z",
    "nvd_published_at" : null
  }
}