GHSA-2XMW-F8J8-WFXC

Vulnerability from github – Published: 2026-07-28 22:25 – Updated: 2026-07-28 22:25
VLAI
Summary
Pagy I18n locale option is not validated before being used in a file path
Details

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:

# 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:

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.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "RubyGems",
        "name": "pagy"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "43.0.0"
            },
            {
              "fixed": "43.5.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-54659"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-200",
      "CWE-22"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-28T22:25:29Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "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 (`\u003clocale\u003e.yml`). An\napplication that assigns untrusted input to the locale \u2014 e.g. the common\npattern `Pagy::I18n.locale = params[:locale]` \u2014 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(\u0026: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\u0027s subsequent structural check (`dictionary[\u0027pagy\u0027][\u0027p11n\u0027]`)\nprevents the file\u0027s 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 `\u003clocale\u003e.yml`, so the request param\ncontrols the target path. For example, pointing it at the app\u0027s\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\u0027s `locales/`\n   directory).\n2. Pagy calls `YAML.load_file` on the resulting `\u2026/config/database.yml`.\n3. The outcome differs by whether that `.yml` exists, is readable, parses\n   as YAML, and has Pagy\u0027s expected structure \u2014 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.",
  "id": "GHSA-2xmw-f8j8-wfxc",
  "modified": "2026-07-28T22:25:29Z",
  "published": "2026-07-28T22:25:29Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Pagy I18n locale option is not validated before being used in a file path"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…