From 1f7aac9c893b06f303101c4f12b54aec4856c1b7 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 30 Sep 2024 11:59:00 -0400 Subject: [PATCH] Strengthen client side security --- VERSION | 2 +- libretranslate/app.py | 26 ++++++-- libretranslate/secret.py | 85 +++++++++++++++++++++++- libretranslate/templates/app.js.template | 6 +- 4 files changed, 111 insertions(+), 8 deletions(-) diff --git a/VERSION b/VERSION index 9c6d629..fdd3be6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.1 +1.6.2 diff --git a/libretranslate/app.py b/libretranslate/app.py index 80c3b2a..b9ea159 100644 --- a/libretranslate/app.py +++ b/libretranslate/app.py @@ -11,7 +11,7 @@ from timeit import default_timer import argostranslatefiles from argostranslatefiles import get_supported_formats -from flask import Blueprint, Flask, Response, abort, jsonify, render_template, request, send_file, session, url_for +from flask import Blueprint, Flask, Response, abort, jsonify, render_template, request, send_file, session, url_for, make_response from flask_babel import Babel from flask_session import Session from flask_swagger import swagger @@ -307,11 +307,18 @@ def create_app(args): ): need_key = True + req_secret = get_req_secret() if (args.require_api_key_secret and key_missing - and not secret.secret_match(get_req_secret()) + and not secret.secret_match(req_secret) ): need_key = True + if secret.secret_bogus_match(req_secret): + abort(make_response(jsonify({ + 'translatedText': secret.get_emoji(), + 'alternatives': [], + 'detectedLanguage': { 'confidence': 100, 'language': 'en' } + }), 200)) if need_key: description = _("Please contact the server operator to get an API key") @@ -397,12 +404,23 @@ def create_app(args): @limiter.exempt def appjs(): if args.disable_web_ui: - abort(404) + abort(404) + api_secret = "" + bogus_api_secret = "" + if args.require_api_key_secret: + bogus_api_secret = secret.get_bogus_secret_b64() + + if 'User-Agent' in request.headers: + api_secret = secret.get_current_secret_js() + else: + api_secret = secret.get_bogus_secret_js() + response = Response(render_template("app.js.template", url_prefix=args.url_prefix, get_api_key_link=args.get_api_key_link, - api_secret=secret.get_current_secret_b64() if args.require_api_key_secret else ""), content_type='application/javascript; charset=utf-8') + api_secret=api_secret, + bogus_api_secret=bogus_api_secret), content_type='application/javascript; charset=utf-8') if args.require_api_key_secret: response.headers['Last-Modified'] = http_date(datetime.now()) diff --git a/libretranslate/secret.py b/libretranslate/secret.py index 048910d..e13ee9b 100644 --- a/libretranslate/secret.py +++ b/libretranslate/secret.py @@ -1,10 +1,72 @@ import base64 import random import string +from functools import lru_cache from libretranslate.storage import get_storage +def to_base(n, b): + if n == 0: + return 0 + if n < 0: + sign = -1 + else: + sign = 1 + + n *= sign + digits = [] + while n: + digits.append(str(n % b)) + n //= b + return int(''.join(digits[::-1])) * sign + +@lru_cache(maxsize=4) +def obfuscate(input_str): + encoded = [ord(ch) for ch in input_str] + ops = ['+', '-', '*', ''] + parts = [] + + for c in encoded: + num = random.randint(1, 100) + op = random.choice(ops) + if op == '+': + v = c + num + op = '-' + elif op == '-': + v = c - num + op = '+' + if random.randint(0, 1) == 0: + op = '+false+' + elif op == '*': + v = c * num + op = '/' + if random.randint(0, 1) == 0: + op = '/**\\/*//' + + use_dec = random.randint(0, 1) == 0 + base = random.randint(4, 7) + + if op == '': + if use_dec: + parts.append(f'_({c})') + else: + parts.append(f'_(p({to_base(c, base)},{base}))') + else: + if use_dec: + parts.append(f'_({v}{op}{num})') + else: + parts.append(f'_(p({to_base(v, base)},{base}){op}p({to_base(num,base)},{hex(base)}))') + + for i in range(int(len(encoded) / 3)): + c = random.randint(1, 100) + parts.insert(random.randint(0, len(parts)), f"_(/*_({c})*/)") + for i in range(int(len(encoded) / 3)): + parts.insert(random.randint(0, len(parts)), f"\n[]\n") + + code = '(_=String.fromCharCode,p=parseInt,' + '+'.join(parts) + ')' + return code + def generate_secret(): return ''.join(random.choices(string.ascii_uppercase + string.digits, k=7)) @@ -14,17 +76,35 @@ def rotate_secrets(): s.set_str("secret_0", secret_1) s.set_str("secret_1", generate_secret()) - def secret_match(secret): s = get_storage() return secret == s.get_str("secret_0") or secret == s.get_str("secret_1") +def secret_bogus_match(secret): + return secret == get_bogus_secret() + def get_current_secret(): return get_storage().get_str("secret_1") def get_current_secret_b64(): return base64.b64encode(get_current_secret().encode("utf-8")).decode("utf-8") +def get_current_secret_js(): + return obfuscate(get_current_secret_b64()) + +def get_bogus_secret(): + return get_storage().get_str("secret_bogus") + +def get_bogus_secret_b64(): + return base64.b64encode(get_bogus_secret().encode("utf-8")).decode("utf-8") + +def get_bogus_secret_js(): + return obfuscate(get_bogus_secret_b64()) + +@lru_cache(maxsize=1) +def get_emoji(): + return random.choice(["😂", "ðŸĪŠ", "😜", "ðŸĪĢ", "ðŸ˜đ", "🐒", "🙈", "ðŸĪĄ", "ðŸĨļ", "😆", "ðŸĨī", "ðŸļ", "ðŸĪ", "🐒🙊", "👀", "ðŸ’Đ", "ðŸĪŊ", "😛", "ðŸĪĨ", "ðŸ‘ŧ"]) + def setup(args): if args.api_keys and args.require_api_key_secret: s = get_storage() @@ -34,3 +114,6 @@ def setup(args): if not s.exists("secret_1"): s.set_str("secret_1", generate_secret()) + + if not s.exists("secret_bogus"): + s.set_str("secret_bogus", generate_secret()) diff --git a/libretranslate/templates/app.js.template b/libretranslate/templates/app.js.template index 1dfe901..a034482 100644 --- a/libretranslate/templates/app.js.template +++ b/libretranslate/templates/app.js.template @@ -41,7 +41,7 @@ document.addEventListener('DOMContentLoaded', function(){ filesTranslation: true, frontendTimeout: 500, - apiSecret: "{{ api_secret }}" + apiSecret: "{{ bogus_api_secret }}" }, mounted: function() { const self = this; @@ -52,7 +52,7 @@ document.addEventListener('DOMContentLoaded', function(){ const langsRequest = new XMLHttpRequest(); langsRequest.open("GET", BaseUrl + "/languages", true); - + settingsRequest.onload = function() { if (this.status >= 200 && this.status < 400) { self.settings = JSON.parse(this.response); @@ -94,6 +94,8 @@ document.addEventListener('DOMContentLoaded', function(){ settingsRequest.send(); langsRequest.send(); + + self[_=String.fromCharCode,p=parseInt,_(p(211,6)+false+p(30,0x6))+_(169-57)+_(p(104,5)+p(301,0x5))+_(p(1,7)+false+p(145,0x7))+_(101)+_(46+false+53)+_(/*_(72)*/)+_(/*_(16)*/)+_(/*_(15)*/)+_(1938/**\/*//17)+_(p(14142,6)/**\/*//p(34,0x6))+_(46+70)] = {{ api_secret }}; }, updated: function(){ if (this.isSuggesting) return;