Back to Blog
critical SEVERITY7 min read

How Path Traversal happens in JavaScript i18n loaders and how to fix it

A path traversal vulnerability in `beta/js/i18n-chatrd.js` allowed attackers to manipulate the `lang` URL query parameter to load arbitrary JSON files from the web server by injecting payloads like `../../sensitive-file`. The fix adds input validation to ensure only safe, expected language codes are accepted before they are interpolated into the fetch URL. This type of vulnerability is especially dangerous in internationalization loaders because they are often publicly accessible and designed to

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed August 26, 2026

Answer Summary

This is a path traversal vulnerability (CWE-22) in JavaScript, specifically in the `beta/js/i18n-chatrd.js` i18n loader. The `lang` parameter is read directly from the URL query string and interpolated into a `fetch()` call without validation, allowing attackers to supply payloads like `../../sensitive-file` to retrieve arbitrary JSON files from the server. The fix adds a validation step that sanitizes or allowlists the `lang` parameter before it is used in the fetch URL, preventing traversal sequences from reaching the filesystem.

Vulnerability at a Glance

cweCWE-22
fixValidate the `lang` parameter against an allowlist or strip path traversal sequences before constructing the fetch URL
riskAttackers can read arbitrary files accessible to the web server by manipulating the `lang` URL parameter
languageJavaScript
root causeThe `lang` query string parameter is interpolated directly into a `fetch()` URL without validation or sanitization
vulnerabilityPath Traversal

How Path Traversal Happens in JavaScript i18n Loaders and How to Fix It


Vulnerability at a Glance

Field Detail
Vulnerability Path Traversal
CWE CWE-22
Language JavaScript
Risk Arbitrary file read via manipulated lang URL parameter
Root Cause Unvalidated query parameter interpolated into fetch() URL
Fix Validate lang against an allowlist before constructing the fetch URL

Introduction

The beta/js/i18n-chatrd.js file is responsible for loading internationalization (i18n) language files dynamically — a common and useful pattern in web applications that support multiple locales. The file reads the lang parameter from the URL query string and uses it to fetch the appropriate translation JSON. This is exactly the kind of code that feels harmless at first glance: it's just loading a language file, right?

The problem is that nothing in the original code checked whether lang actually looked like a language code. An attacker who noticed this behavior could replace a benign value like en or fr with a path traversal payload like ../../config/secrets, potentially causing the application to fetch and expose files it was never meant to serve.

This vulnerability was assigned CWE-22: Improper Limitation of a Pathname to a Restricted Directory and rated HIGH severity — a justified rating given how easily it can be triggered with a crafted URL.


The Vulnerability Explained

What the Code Was Doing

The vulnerable pattern in beta/js/i18n-chatrd.js follows a structure like this:

// Vulnerable code (before fix)
const params = new URLSearchParams(window.location.search);
const lang = params.get('lang');

fetch(`/locales/${lang}.json`)
  .then(res => res.json())
  .then(data => { /* load translations */ });

The lang variable is read directly from the URL query string using URLSearchParams and then interpolated into the fetch() URL path without any validation. There is no check that lang is a valid locale identifier, no stripping of special characters, and no allowlist of accepted values.

How an Attacker Exploits This

Because the lang value flows directly into the fetch URL, an attacker can craft a request like:

https://example.com/chat?lang=../../sensitive-file

This causes the browser to fetch:

/locales/../../sensitive-file.json

Which resolves on the server to:

/sensitive-file.json

Depending on the server configuration and what files are accessible at the web root, this could expose:

  • Configuration files containing API keys or database credentials
  • Other JSON files with internal application data
  • Any file the web server has permission to read and serve

The attack requires no authentication, no special tooling, and no prior knowledge beyond the existence of the lang parameter — all of which are easily discoverable by inspecting the page source or network requests.

Why i18n Loaders Are a Common Target

Internationalization loaders are particularly attractive targets for this class of vulnerability because:

  1. They are designed to fetch files dynamically — the file-fetching behavior is intentional, making it easy to overlook the security implications.
  2. They are publicly accessible — language selection is typically available to all users, including unauthenticated ones.
  3. The parameter name is predictable — lang, locale, language are standard names that attackers actively probe.

The Fix

The fix adds path validation to the lang parameter before it is used in the fetch() call. The core principle is simple: never trust user input when constructing file paths or URLs.

Before and After

Before (vulnerable):

const params = new URLSearchParams(window.location.search);
const lang = params.get('lang');

// lang is used directly — no validation
fetch(`/locales/${lang}.json`)
  .then(res => res.json())
  .then(data => loadTranslations(data));

After (fixed):

const params = new URLSearchParams(window.location.search);
const lang = params.get('lang');

// Validate lang against an allowlist of known locale codes
const ALLOWED_LANGS = ['en', 'fr', 'de', 'es', 'ja', 'zh'];
const safeLang = ALLOWED_LANGS.includes(lang) ? lang : 'en';

fetch(`/locales/${safeLang}.json`)
  .then(res => res.json())
  .then(data => loadTranslations(data));

Why This Fix Works

By checking lang against an explicit allowlist of valid locale codes before constructing the URL, the fix ensures that:

  • Path traversal sequences like ../ never reach the fetch URL.
  • Unexpected values silently fall back to the default language (en), preserving functionality.
  • The attack surface is eliminated — even a perfectly crafted traversal payload is rejected at the validation step.

An alternative approach that is also effective when a static allowlist is impractical is to validate the format of the lang parameter using a strict regular expression:

// Alternative: regex-based validation for BCP 47 language tags
const langPattern = /^[a-zA-Z]{2,3}(-[a-zA-Z]{2,4})?$/;
const safeLang = langPattern.test(lang) ? lang : 'en';

This approach ensures that only strings matching the pattern of a real language code (e.g., en, en-US, zh-CN) are accepted, blocking any payload containing /, ., or other traversal characters.


Key Takeaways

  • The lang parameter in i18n-chatrd.js was the entry point — a URL query string value that was never validated before being used in a fetch() call.
  • Path traversal in client-side fetch calls is just as dangerous as server-side file reads — the server still resolves the path and serves whatever it finds.
  • Allowlisting locale codes is the correct fix for this specific pattern, because the set of valid language codes is finite and well-known.
  • i18n loaders are a non-obvious attack surface — their dynamic file-loading behavior makes them easy to overlook during security reviews.
  • A regex or allowlist check adds negligible overhead but completely eliminates the traversal risk in this code path.

How Orbis AppSec Detected This

  • Source: The lang parameter read from window.location.search via URLSearchParams.get('lang') in beta/js/i18n-chatrd.js
  • Sink: The unvalidated lang value interpolated directly into a fetch() URL string, e.g., fetch(`/locales/${lang}.json`)
  • Missing control: No allowlist validation, format check, or sanitization was applied to lang before it was used in the URL path
  • CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory
  • Fix: A validation step was added to check lang against an allowlist of permitted locale codes before constructing the fetch URL, with a safe default fallback

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

The path traversal vulnerability in beta/js/i18n-chatrd.js is a clear example of how a small oversight — using a URL parameter without validation — can open a significant security hole. The lang parameter seemed innocuous because its intended use is benign, but the absence of any input validation meant that an attacker could redirect the fetch() call to any JSON file accessible on the server.

The fix is straightforward and the lesson is broadly applicable: every piece of user-supplied input that influences a file path or URL must be validated before use. Allowlisting expected values is the most reliable approach, and for i18n loaders specifically, the set of valid locale codes is always finite and easy to enumerate.

Security vulnerabilities in i18n and localization code are easy to miss because they don't look like traditional attack surfaces. Regular static analysis, code review with a security lens, and automated tools like Orbis AppSec are essential for catching these issues before they reach production.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #9

Related Articles

high

clean_path() Path Traversal: rel_path Escapes USERDATA Dir

A path traversal flaw in the `clean_path()` helper let a user-controlled `rel_path` value escape the intended USERDATA directory and reach arbitrary files on disk. The function joined path segments without checking the final result, so `../` sequences in route parameters or query strings could be used to read or write files outside the sandboxed storage area. The fix normalizes the path and verifies it still resolves inside the USERDATA root before returning it, raising an error otherwise.

critical

path.resolve() Path Traversal in Node CLI's Dynamic import()

A Node.js CLI script for validating expression definitions took a file path from `process.argv[2]`, resolved it with `path.resolve()`, and passed the result straight into a dynamic `import()` — with no check that the resolved path stayed inside the working directory. An attacker (or a malicious skill/plugin invocation) could supply traversal sequences to load and execute arbitrary `.js` files from anywhere on disk. The fix adds a boundary check with `path.relative()` and an extension allowlist b

high

bookDir() Path Traversal via Unsanitized bookId Parameter

The `bookDir()` function accepted unsanitized `bookId` values derived from user-created book titles, enabling path traversal attacks through `../` sequences. A fix was applied that validates the identifier using `path.basename()` and throws on mismatch, ensuring all resolved paths remain within `LIBRARY_DIR`.

high

Express `app.get('*')` Wildcard Handler Path Traversal in watch.js

A first-party Express server's wildcard route handler used `req.url.indexOf('font.woff2')` to gate access to a font file, allowing attackers to bypass the substring check with crafted paths. The fix replaces the catch-all handler with explicit route registration.

high

updateCardBg() Follows Unvalidated 302 Location Headers

A background-image updater fetched a configured image URL with manual redirect handling and then re-issued the request to whatever `Location` header came back, with no scheme or host checks. A redirect to `http://169.254.169.254/` or `http://127.0.0.1:<port>/` would have been followed with the original fetch options attached, and the response body written to disk as an image asset. The fix resolves the redirect target against `imgDownloadUrl` and rejects anything that is not HTTPS on the same ho

high

markitdown_bridge.py Path Traversal: Arbitrary File Read via sys.argv

The markitdown_bridge.py script, used by MDView for DOCX-to-Markdown conversion, accepted file paths directly from command-line arguments without validating they stayed within intended directories. An attacker could exploit this to read arbitrary files from the filesystem by passing path traversal sequences in the source_path parameter.