Files
ALPHA-TRAIN2/services/rusgram/content/content.py
T
frogolonso 526374b5a6
build-and-push / detect (push) Successful in 12s
build-and-push / build (${{ fromJSON(needs.detect.outputs.services) }}) (push) Successful in 25s
hueta2
2026-08-26 13:06:59 +03:00

114 lines
5.2 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/python3
from flask import Flask, render_template, request, redirect, abort
import os
import requests as r
app = Flask(__name__, template_folder='files')
database_node_port = 5002
content_node_port = 5004
database_node = "database:" + str(database_node_port)
INTERNAL = os.environ.get("RUSGRAM_INTERNAL", "e9b2f6d14a8c7035e1d0a6b8c4f2e7a9")
DBH = {"X-Internal": INTERNAL}
# PATCH V1/V5: template_folder ('files') — это тот же том, где лежит db/users/<логин>.
# Раньше render_template(path) с путём из URL позволял отрендерить файл юзера
# как Jinja-шаблон => SSTI => RCE. Рендерим только реально существующие страницы.
ALLOWED_TEMPLATES = {"index.html", "login.html", "register.html", "specific.html"}
# PATCH V8: сколько картинок реально есть в db/images
IMG_MIN, IMG_MAX = 1, 16
# PATCH V9: чекер хранит флаг как КОММЕНТАРИЙ к картинке, а specific.html
# раньше отдавал ВСЕ комментарии любому залогиненному юзеру => атакующий
# регистрируется и читает чужие флаги через парадный вход. Показываем юзеру
# только его собственные комментарии. Чекер постит и читает свой коммент одной
# сессией (см. test_rusgram.py) — легальный флоу не ломается.
# Отключить (если чекер читает чужие комменты): RUSGRAM_COMMENT_PRIVACY=0
COMMENT_PRIVACY = os.environ.get("RUSGRAM_COMMENT_PRIVACY", "1") != "0"
def get_login():
"""Возвращает имя юзера или None. Никогда не бросает исключение."""
session = request.cookies.get("session")
if session is None:
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 _field(username, name):
try:
resp = r.get(f"http://{database_node}/users/{username}/{name}",
headers=DBH, timeout=5)
return resp.json()["data"] if resp.status_code == 200 else ""
except (r.RequestException, ValueError, KeyError):
return ""
@app.route('/<path:path>', methods=["GET", "POST"])
def route(path):
if path == "index.html":
if (username := get_login()) is None:
return redirect("/login.html", code=302)
imgs = list()
for i in range(IMG_MIN, IMG_MAX + 1):
try:
imgs.append(r.get(f"http://{database_node}/images/{i}",
headers=DBH, timeout=5).json()["data"])
except (r.RequestException, ValueError, KeyError):
continue
imgs[-1]["id"] = str(i).zfill(2)
user = dict()
user["first_name"] = _field(username, "first_name")
user["second_name"] = _field(username, "second_name")
user["email"] = _field(username, "email")
user["msg"] = _field(username, "msg")
return render_template("index.html", data=imgs, user=user)
elif path == "specific.html":
username = get_login()
if username is None:
return redirect("/login.html", code=302)
# PATCH V8: img приходит из URL. Раньше любое нечисловое/вне диапазона
# значение давало 500, а незакавыченная подстановка {{img['id']}} внутрь
# <script> в шаблоне позволяла подмешать JS.
try:
img_id = int(request.args.get('img', ''))
except (TypeError, ValueError):
abort(404)
if not (IMG_MIN <= img_id <= IMG_MAX):
abort(404)
try:
img = r.get(f"http://{database_node}/images/{img_id}",
headers=DBH, timeout=5).json()["data"]
except (r.RequestException, ValueError, KeyError):
abort(404)
img["id"] = str(img_id).zfill(2)
# PATCH V9: не отдаём чужие комментарии (в них лежат флаги).
if COMMENT_PRIVACY:
comments = img.get("comments", [])
img["comments"] = [c for c in comments
if isinstance(c, list) and len(c) == 2 and c[0] == username]
return render_template("specific.html", img=img)
else:
# PATCH V1/V5: всё, чего нет в белом списке, до Jinja не доходит.
if path not in ALLOWED_TEMPLATES:
abort(404)
return render_template(path)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=content_node_port)