RubyGems · pagy
Pagy I18n locale option is not validated before being used in a file path
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.
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.
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:
?locale=../../../config/database (adjust the
number of ../ to reach the app root from the gem's locales/
directory).YAML.load_file on the resulting …/config/database.yml..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.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.
Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.
Check my repoSources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.