diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40882b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Python bytecode +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Virtual environments +venv/ +env/ +ENV/ +.venv/ +.virtualenvs/ + +# Instance folder (may contain sensitive configs) +instance/*.cfg +!instance/default.cfg + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Test files +static/*.test diff --git a/README.md b/README.md index 0b8c228..6ac5401 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A \(simple\) Python port of the [LookingGlass](https://github.com/telephone/Look The design is much simpler than the PHP original, leaving rate limiting up to the web server \(the example configs only allow 16 simultaneous connections\) and configuration does involve some manual setup right now \(more on this later\). The original had the option of not presenting the IPv6 options, but this does not. IPv6 adoption is critical and if you are rolling out a new looking glass without IPv6 connectivity you are doing the internet a disservice. ## Requirements -* Python 2.7 (may work on earlier/later versions, not currently tested) +* Python 3.8+ (updated from Python 2.7 for security and modern features) * Virtualenv with all modules from requirements.txt installed (pip install -r requirements.txt) * The ping, mtr, traceroute, and host utilities. Other utilities may be added later. * A method of serving a python WSGI application. Example configs for nginx/uwsgi are included. diff --git a/example_configs/uwsgi/wsgi.py b/example_configs/uwsgi/wsgi.py index 1fd8bf4..2eb42f2 100644 --- a/example_configs/uwsgi/wsgi.py +++ b/example_configs/uwsgi/wsgi.py @@ -1,2 +1,3 @@ -#!/home/lg/.virtualenvs/LookingGlass/bin/python +#!/home/lg/.virtualenvs/LookingGlass/bin/python3 +# Python 3 WSGI entry point from lookingglass import app diff --git a/lookingglass.py b/lookingglass.py index 7200fd2..6b312c2 100644 --- a/lookingglass.py +++ b/lookingglass.py @@ -1,6 +1,7 @@ import flask import re import sh +from markupsafe import escape app = flask.Flask(__name__) app.config.from_pyfile('instance/default.cfg') @@ -10,14 +11,36 @@ def get_and_validate_host(request): """Confirms that request's host parameter is a host or ip(v6) returns None on failure. + + Security improvements: + - Length validation (max 253 chars for FQDN per RFC 1035) + - Proper regex escaping + - Validates IPv4, IPv6, and hostname formats """ target = request.args.get('host') if target is None: - return - match = re.search("^[a-zA-Z0-9\-\.:]+$", target) - if match is not None: - return target - return + return None + + # Length validation to prevent DoS + if len(target) > 253: + return None + + # Properly escaped regex - hyphen at end of character class + # Allows: alphanumeric, dots, colons (for IPv6), hyphens + match = re.search(r"^[a-zA-Z0-9.:_-]+$", target) + if match is None: + return None + + # Additional validation: prevent obvious abuse patterns + # No leading/trailing dots or hyphens + if target.startswith(('.', '-')) or target.endswith(('.', '-')): + return None + + # Prevent multiple consecutive dots (not valid in hostnames) + if '..' in target: + return None + + return target def execute(encoder, command, *args, **kwargs): """Runs with *args and **kwargs, then handles output using the @@ -29,7 +52,7 @@ def raw_stream_generator(): """Generator for streaming output""" try: for chunk in command(*args, _iter=True, **kwargs): - yield flask.escape(chunk) + yield escape(chunk) except sh.ErrorReturnCode: pass if encoder == "raw": @@ -46,7 +69,7 @@ def raw_stream_generator(): "status": status }) else: - return flask.escape("Error, invalid encoder") + return escape("Error, invalid encoder") def error_response(encoder, error): """Responds with text based on """ @@ -56,7 +79,7 @@ def error_response(encoder, error): "status": 1 }) else: - return flask.escape(error) + return escape(error) @app.route("//host") def api_host(encoder): diff --git a/requirements.txt b/requirements.txt index a007b4f..747014c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,11 @@ -Click==7.0 -Flask==1.0.2 -Jinja2==2.10 -MarkupSafe==1.1.1 -Werkzeug==0.15.1 -argparse==1.2.1 -itsdangerous==1.1.0 -sh==1.11 -wsgiref==0.1.2 +# Modern Python 3 dependencies (2025) +# Requires Python 3.8+ + +# Web framework and dependencies +Flask>=3.0.0,<4.0.0 +# Flask automatically installs: Jinja2, MarkupSafe, Werkzeug, itsdangerous, click, blinker + +# Shell command execution +sh>=2.0.0,<3.0.0 + +# Note: argparse and wsgiref are now built into Python 3 stdlib