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:
- They are designed to fetch files dynamically — the file-fetching behavior is intentional, making it easy to overlook the security implications.
- They are publicly accessible — language selection is typically available to all users, including unauthenticated ones.
- The parameter name is predictable —
lang,locale,languageare 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.
Prevention & Best Practices
1. Always Validate External Input Before Using It in Paths or URLs
Any value that originates from user input — URL parameters, form fields, cookies, headers — must be validated before being used to construct file paths, URLs, or database queries. This is the foundational rule of secure input handling.
2. Prefer Allowlists Over Denylists
It is tempting to try to block known-bad patterns like ../ or %2e%2e. However, attackers have a large repertoire of encoding tricks to bypass denylist filters:
../→%2e%2e%2f../→..%2f../→%2e%2e/
An allowlist of valid values (or a strict format validation) is far more robust because it only permits what you explicitly expect.
3. Use path.basename() as a Defense-in-Depth Measure (Node.js)
If you are constructing file paths on the server side in Node.js, path.basename() strips all directory components from a path, leaving only the filename:
const path = require('path');
const safeFile = path.basename(userInput); // '../../etc/passwd' → 'passwd'
This is not a complete fix on its own but is a useful defense-in-depth layer.
4. Apply the Same Validation to Similar Parameters
In any codebase with i18n loaders, check for similar patterns in other files. Parameters named locale, language, theme, template, page, or file are all common candidates for path traversal vulnerabilities.
5. Security Standards and References
- OWASP Path Traversal: https://owasp.org/www-community/attacks/Path_Traversal
- CWE-22: https://cwe.mitre.org/data/definitions/22.html
- OWASP Input Validation Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
Key Takeaways
- The
langparameter ini18n-chatrd.jswas the entry point — a URL query string value that was never validated before being used in afetch()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
langparameter read fromwindow.location.searchviaURLSearchParams.get('lang')inbeta/js/i18n-chatrd.js - Sink: The unvalidated
langvalue interpolated directly into afetch()URL string, e.g.,fetch(`/locales/${lang}.json`) - Missing control: No allowlist validation, format check, or sanitization was applied to
langbefore 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
langagainst 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.