#!/usr/bin/python3 from flask import Flask, make_response, request, redirect import os import requests as r app = Flask(__name__) database_node_port = 5002 logic_node_port = 5003 database_node = "database:" + str(database_node_port) IMG_MIN, IMG_MAX = 1, 16 INTERNAL = os.environ.get("RUSGRAM_INTERNAL", "e9b2f6d14a8c7035e1d0a6b8c4f2e7a9") DBH = {"X-Internal": INTERNAL} def bad_request(status="error", code=400): return make_response({"status": status}, code) def unauthorized(): """PATCH V3/V6: раньше здесь был redirect(request.headers.get("Origin","") + "/login.html"). Origin контролирует атакующий, а proxy ходит по Location серверсайд => SSRF во внутреннюю сеть. Location теперь фиксированный и относительный.""" return redirect("/login.html", code=302) def valid_username(username): """PATCH V1/V8 (defense in depth): запрещаем только то, что реально опасно. Остальное (юникод, точки внутри имени) оставляем — чтобы не сломать чекер.""" if not username or len(username) > 64: return False if "||" in username: # ломает формат куки session=|| return False if "/" in username or "\\" in username or username in (".", ".."): return False # имя юзера = имя файла в db/users/ if username.lower().endswith((".html", ".htm")): return False # имя, которое content мог бы отрендерить как шаблон if "{{" in username or "{%" in username or "{#" in username: return False return True def clean_value(value): """Не даём записать Jinja-конструкции в поля, которые когда-то рендерились как шаблон.""" return isinstance(value, str) and "{{" not in value and "{%" not in value and "{#" not in value def current_user(): """Возвращает имя авторизованного юзера или None. Не бросает исключений.""" session = request.cookies.get("session") if session is None: # PATCH V8: раньше KeyError -> 500 return None parts = session.split("||") if len(parts) != 2: # PATCH V8: раньше ValueError -> 500 return None username, password = parts if not username: return None try: resp = r.get(f"http://{database_node}/users/{username}/password", headers=DBH, timeout=5) if resp.status_code != 200: # PATCH V8: несуществующий юзер -> KeyError -> 500 return None if resp.json().get("data") != password: return None except (r.RequestException, ValueError): return None return username def json_body(): data = request.get_json(silent=True) return data if isinstance(data, dict) else None @app.route('/login', methods=["POST"]) def login(): data = json_body() if data is None or "login" not in data or "password" not in data: return bad_request() username, password = data["login"], data["password"] if not isinstance(username, str) or not isinstance(password, str): return bad_request() try: resp = r.get(f"http://{database_node}/users/{username}/password", headers=DBH, timeout=5) correct_password = resp.json()["data"] if resp.status_code == 200 else None except (r.RequestException, ValueError, KeyError): return bad_request() if correct_password is not None and correct_password == password: out = make_response({"status": "ok"}, 200) out.set_cookie("session", f"{username}||{password}", httponly=True, samesite="Lax") return out return make_response({"status": "wrong_creds"}, 401) @app.route('/register', methods=["POST"]) def register(): data = json_body() if data is None or "login" not in data or "password" not in data: return bad_request() username, password = data["login"], data["password"] if not isinstance(username, str) or not isinstance(password, str): return bad_request() if not valid_username(username): return make_response({"status": "bad_login"}, 400) # PATCH V2: раньше регистрация существующего юзера просто ПЕРЕЗАПИСЫВАЛА # его пароль (остальные поля, включая флаг, сохранялись) => полный захват # чужого аккаунта без единого запроса на аутентификацию. try: exists = r.get(f"http://{database_node}/users/{username}/password", headers=DBH, timeout=5) if exists.status_code == 200: return make_response({"status": "user_exists"}, 409) except r.RequestException: return bad_request() try: resp = r.post(f"http://{database_node}/users/{username}/password", json={"value": password}, headers=DBH, timeout=5) except r.RequestException: return bad_request() if resp.status_code == 200: out = make_response({"status": "ok"}, 200) out.set_cookie("session", f"{username}||{password}", httponly=True, samesite="Lax") return out return make_response({"status": "error"}, resp.status_code) @app.route('/update_personal', methods=["POST"]) def update_personal(): username = current_user() if username is None: return unauthorized() data = json_body() if data is None: return bad_request() for field in ("first_name", "second_name", "email"): if field not in data or not clean_value(data[field]): return bad_request() try: for field in ("first_name", "second_name", "email"): r.post(f"http://{database_node}/users/{username}/{field}", json={"value": data[field]}, headers=DBH, timeout=5) except r.RequestException: return bad_request() return make_response({"status": "ok"}, 200) @app.route('/send_msg', methods=["POST"]) def send_msg(): username = current_user() if username is None: return unauthorized() data = json_body() if data is None or not clean_value(data.get("msg")): return bad_request() try: r.post(f"http://{database_node}/users/{username}/msg", json={"value": data["msg"]}, headers=DBH, timeout=5) except r.RequestException: return bad_request() return make_response({"status": "ok"}, 200) @app.route('/post_comment', methods=["POST"]) def post_comment(): username = current_user() if username is None: return unauthorized() data = json_body() if data is None or not clean_value(data.get("msg")): return bad_request() # PATCH V8: img_id раньше уходил в URL внутреннего запроса как есть try: img_id = int(data.get("img_id")) except (TypeError, ValueError): return bad_request() if not (IMG_MIN <= img_id <= IMG_MAX): return bad_request() try: r.post(f"http://{database_node}/images/{img_id}", json={"value": [username, data["msg"]]}, headers=DBH, timeout=5) except r.RequestException: return bad_request() return make_response({"status": "ok"}, 200) if __name__ == '__main__': app.run(host="0.0.0.0", port=logic_node_port)