CVE-2023-48711: Server-Side Request Forgery (SSRF) (CWE-918)
value: google-translate-api-browser is an npm package which interfaces with the google translate web api. A Server-Side Request Forgery (SSRF) Vulnerability is present in applications utilizing the `google-translate-api-browser` package and exposing the `translateOptions` to the end user. An attacker can set a malicious `tld`, causing the application to return unsafe URLs pointing towards local resources. The `translateOptions.tld` field is not properly sanitized before being placed in the Google translate URL. This can allow an attacker with control over the `translateOptions` to set the `tld` to a payload such as `@127.0.0.1`. This causes the full URL to become `https://translate.google.@127.0.0.1/...`, where `translate.google.` is the username used to connect to localhost. An attacker can send requests within internal networks and the local host. Should any HTTPS application be present on the internal network with a vulnerability exploitable via a GET call, then it would be possible to exploit this using this vulnerability. This issue has been addressed in release version 4.1.3. Users are advised to upgrade. There are no known workarounds for this vulnerability.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 3.7
Published: 2023-11-24T17:15Z
Repository: https://github.com/cjvnjde/google-translate-api-browser
Language: TypeScript
Files changed: package.json, src/generateRequestUrl.ts, src/normaliseResponse.ts, src/translate.server.ts
Commit date: 2023-11-22 23:54:10 +0200
Commit message: Add tld validation

Diff: diff --git a/package.json b/package.json
index 6e3b5cd..5d31d6d 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
   "name": "google-translate-api-browser",
-  "version": "4.1.1",
+  "version": "4.1.3",
   "description": "A free and unlimited API for Google Translate that works in browser",
   "repository": {
     "type": "git",
     "url": "https://github.com/cjvnjde/google-translate-api-browser"
   },
diff --git a/src/generateRequestUrl.ts b/src/generateRequestUrl.ts
index 26419cf..b819178 100644
--- a/src/generateRequestUrl.ts
+++ b/src/generateRequestUrl.ts
@@ -1,11 +1,19 @@
 import { defaultTranslateOptions } from "./defaultTranslateOptions";
 import { TranslateOptions } from "./TranslateOptions";
 
+function validateTLD(tld: string) {
+  return Boolean(tld.match(/^[a-zA-Z]{2,63}$/));
+}
+
 export function generateRequestUrl(options: Partial<Omit<TranslateOptions, 'raw'>> = {}): string {
   const translateOptions = { ...defaultTranslateOptions, ...options };
 
+  if (!validateTLD(translateOptions.tld)) {
+    throw new Error("Invalid TLD: Must be 2-63 letters only")
+  }
+
   const queryParams = {
     rpcids: translateOptions.rpcids,
     'source-path': '/',
     hl: translateOptions.hl,
     'soc-app': '1',
diff --git a/src/normaliseResponse.ts b/src/normaliseResponse.ts
index b01e239..681b45d 100644
--- a/src/normaliseResponse.ts
+++ b/src/normaliseResponse.ts
@@ -27,11 +27,11 @@ export function normaliseResponse(rawBody: string, raw = false): TranslationResu
 
     data = JSON.parse(JSON.parse(valuableContent))
   }
 
   if (!data) {
-    throw new Error('No data')
+    throw new Error('Data is either empty or corrupted')
   }
 
   const result: TranslationResult = {
     text: data[1][0][0][5][0][0],
     pronunciation: data[0][0],
diff --git a/src/translate.server.ts b/src/translate.server.ts
index 838b441..1e0302e 100644
--- a/src/translate.server.ts
+++ b/src/translate.server.ts
@@ -2,17 +2,20 @@ import { generateRequestUrl } from "./generateRequestUrl";
 import { defaultTranslateOptions } from "./defaultTranslateOptions";
 import { TranslateOptions } from "./TranslateOptions";
 import { normaliseResponse, TranslationResult } from "./normaliseResponse";
 import * as https from 'node:https';
 
+export function createRequestBody(text: string, translateOptions: Pick<TranslateOptions, "to" | "from" | "rpcids">) {
+    const encodedData = encodeURIComponent(`[[["${translateOptions.rpcids}","[[\\"${text}\\",\\"${translateOptions.from}\\",\\"${translateOptions.to}\\",true],[1]]",null,"generic"]]]`);
+    return `f.req=${encodedData}&`;
+}
+
 export function translate(text: string, options: Partial<TranslateOptions> = {}): Promise<TranslationResult> {
   const translateOptions = { ...defaultTranslateOptions, ...options };
 
   return new Promise((resolve, reject) => {
-    const encodedData = encodeURIComponent(`[[["${translateOptions.rpcids}","[[\\"${text}\\",\\"${translateOptions.from}\\",\\"${translateOptions.to}\\",true],[1]]",null,"generic"]]]`);
-    const body = `f.req=${encodedData}&`;
-
+    const body = createRequestBody(text, translateOptions);
     const url = generateRequestUrl(translateOptions);
 
     const req = https.request(url, {
       method: 'POST',
       headers: {
@@ -27,13 +30,11 @@ export function translate(text: string, options: Partial<TranslateOptions> = {})
       });
 
       resp.on('end', () => {
         resolve(normaliseResponse(data))
       });
-    }).on('error', (err) => {
-      reject(err)
-    })
+    }).on('error', reject);
 
     req.write(body);
     req.end();
   })
 }

Vulnerable code:   "version": "4.1.1",
    throw new Error('No data')
    const encodedData = encodeURIComponent(`[[["${translateOptions.rpcids}","[[\\"${text}\\",\\"${translateOptions.from}\\",\\"${translateOptions.to}\\",true],[1]]",null,"generic"]]]`);
    const body = `f.req=${encodedData}&`;

    }).on('error', (err) => {
      reject(err)
    })
Fixed code:   "version": "4.1.3",
function validateTLD(tld: string) {
  return Boolean(tld.match(/^[a-zA-Z]{2,63}$/));
}

  if (!validateTLD(translateOptions.tld)) {
    throw new Error("Invalid TLD: Must be 2-63 letters only")
  }

    throw new Error('Data is either empty or corrupted')
export function createRequestBody(text: string, translateOptions: Pick<TranslateOptions, "to" | "from" | "rpcids">) {
    const encodedData = encodeURIComponent(`[[["${translateOptions.rpcids}","[[\\"${text}\\",\\"${translateOptions.from}\\",\\"${translateOptions.to}\\",true],[1]]",null,"generic"]]]`);
    return `f.req=${encodedData}&`;
}

    const body = createRequestBody(text, translateOptions);
    }).on('error', reject);