VYPR
Moderate severityNVD Advisory· Published Jan 23, 2024· Updated Jun 17, 2025

Whoogle Search Limited File Write vulnerability

CVE-2024-22204

Description

Whoogle Search is a self-hosted metasearch engine. Versions 0.8.3 and prior have a limited file write vulnerability when the configuration options in Whoogle are enabled. The config function in app/routes.py does not validate the user-controlled name variable on line 447 and config_data variable on line 437. The name variable is insecurely concatenated in os.path.join, leading to path manipulation. The POST data from the config_data variable is saved with pickle.dump which leads to a limited file write. However, the data that is saved is earlier transformed into a dictionary and the url key value pair is added before the file is saved on the system. All in all, the issue allows us to save and overwrite files on the system that the application has permissions to, with a dictionary containing arbitrary data and the url key value, which is a limited file write. Version 0.8.4 contains a patch for this issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
whoogle-searchPyPI
< 0.8.40.8.4

Affected products

1

Patches

1
3a2e0b262e4a

Validate urls in `element` and `window` endpoints

https://github.com/benbusby/whoogle-searchBen BusbySep 13, 2023via ghsa
1 file changed · +26 7
  • app/routes.py+26 7 modified
    @@ -4,8 +4,10 @@
     import json
     import os
     import pickle
    +import re
     import urllib.parse as urlparse
     import uuid
    +import validators
     from datetime import datetime, timedelta
     from functools import wraps
     
    @@ -420,13 +422,18 @@ def config():
         config_disabled = (
                 app.config['CONFIG_DISABLE'] or
                 not valid_user_session(session))
    +
    +    name = ''
    +    if 'name' in request.args:
    +        name = os.path.normpath(request.args.get('name'))
    +        if not re.match(r'^[A-Za-z0-9_.+-]+$', name):
    +            return make_response('Invalid config name', 400)
    +
         if request.method == 'GET':
             return json.dumps(g.user_config.__dict__)
         elif request.method == 'PUT' and not config_disabled:
    -        if 'name' in request.args:
    -            config_pkl = os.path.join(
    -                app.config['CONFIG_PATH'],
    -                request.args.get('name'))
    +        if name:
    +            config_pkl = os.path.join(app.config['CONFIG_PATH'], name)
                 session['config'] = (pickle.load(open(config_pkl, 'rb'))
                                      if os.path.exists(config_pkl)
                                      else session['config'])
    @@ -444,7 +451,7 @@ def config():
                     config_data,
                     open(os.path.join(
                         app.config['CONFIG_PATH'],
    -                    request.args.get('name')), 'wb'))
    +                    name), 'wb'))
     
             session['config'] = config_data
             return redirect(config_data['url'])
    @@ -463,6 +470,8 @@ def imgres():
     @session_required
     @auth_required
     def element():
    +    empty_gif = base64.b64decode(
    +        'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==')
         element_url = src_url = request.args.get('url')
         if element_url.startswith('gAAAAA'):
             try:
    @@ -475,6 +484,11 @@ def element():
     
         src_type = request.args.get('type')
     
    +    # Ensure requested element is from a valid domain
    +    domain = urlparse.urlparse(src_url).netloc
    +    if not validators.domain(domain):
    +        return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
    +
         try:
             file_data = g.user_request.send(base_url=src_url).content
             tmp_mem = io.BytesIO()
    @@ -485,8 +499,6 @@ def element():
         except exceptions.RequestException:
             pass
     
    -    empty_gif = base64.b64decode(
    -        'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==')
         return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
     
     
    @@ -504,6 +516,13 @@ def window():
             root_url=request.url_root,
             config=g.user_config)
         target = urlparse.urlparse(target_url)
    +
    +    # Ensure requested URL has a valid domain
    +    if not validators.domain(target.netloc):
    +        return render_template(
    +            'error.html',
    +            error_message='Invalid location'), 400
    +
         host_url = f'{target.scheme}://{target.netloc}'
     
         get_body = g.user_request.send(base_url=target_url).text
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

10

News mentions

0

No linked articles in our index yet.