hueta
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
from flask import Flask, render_template, request, redirect, abort
|
from flask import Flask, render_template, request, redirect, abort
|
||||||
|
import os
|
||||||
import requests as r
|
import requests as r
|
||||||
app = Flask(__name__, template_folder='files')
|
app = Flask(__name__, template_folder='files')
|
||||||
database_node_port = 5002
|
database_node_port = 5002
|
||||||
content_node_port = 5004
|
content_node_port = 5004
|
||||||
database_node = "database:" + str(database_node_port)
|
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/<логин>.
|
# PATCH V1/V5: template_folder ('files') — это тот же том, где лежит db/users/<логин>.
|
||||||
# Раньше render_template(path) с путём из URL позволял отрендерить файл юзера
|
# Раньше render_template(path) с путём из URL позволял отрендерить файл юзера
|
||||||
@@ -27,7 +30,8 @@ def get_login():
|
|||||||
if not username:
|
if not username:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
resp = r.get(f"http://{database_node}/users/{username}/password", timeout=5)
|
resp = r.get(f"http://{database_node}/users/{username}/password",
|
||||||
|
headers=DBH, timeout=5)
|
||||||
if resp.status_code != 200: # PATCH V8: раньше KeyError -> 500
|
if resp.status_code != 200: # PATCH V8: раньше KeyError -> 500
|
||||||
return None
|
return None
|
||||||
if resp.json().get("data") != password:
|
if resp.json().get("data") != password:
|
||||||
@@ -39,7 +43,8 @@ def get_login():
|
|||||||
|
|
||||||
def _field(username, name):
|
def _field(username, name):
|
||||||
try:
|
try:
|
||||||
resp = r.get(f"http://{database_node}/users/{username}/{name}", timeout=5)
|
resp = r.get(f"http://{database_node}/users/{username}/{name}",
|
||||||
|
headers=DBH, timeout=5)
|
||||||
return resp.json()["data"] if resp.status_code == 200 else ""
|
return resp.json()["data"] if resp.status_code == 200 else ""
|
||||||
except (r.RequestException, ValueError, KeyError):
|
except (r.RequestException, ValueError, KeyError):
|
||||||
return ""
|
return ""
|
||||||
@@ -53,7 +58,8 @@ def route(path):
|
|||||||
imgs = list()
|
imgs = list()
|
||||||
for i in range(IMG_MIN, IMG_MAX + 1):
|
for i in range(IMG_MIN, IMG_MAX + 1):
|
||||||
try:
|
try:
|
||||||
imgs.append(r.get(f"http://{database_node}/images/{i}", timeout=5).json()["data"])
|
imgs.append(r.get(f"http://{database_node}/images/{i}",
|
||||||
|
headers=DBH, timeout=5).json()["data"])
|
||||||
except (r.RequestException, ValueError, KeyError):
|
except (r.RequestException, ValueError, KeyError):
|
||||||
continue
|
continue
|
||||||
imgs[-1]["id"] = str(i).zfill(2)
|
imgs[-1]["id"] = str(i).zfill(2)
|
||||||
@@ -76,7 +82,8 @@ def route(path):
|
|||||||
if not (IMG_MIN <= img_id <= IMG_MAX):
|
if not (IMG_MIN <= img_id <= IMG_MAX):
|
||||||
abort(404)
|
abort(404)
|
||||||
try:
|
try:
|
||||||
img = r.get(f"http://{database_node}/images/{img_id}", timeout=5).json()["data"]
|
img = r.get(f"http://{database_node}/images/{img_id}",
|
||||||
|
headers=DBH, timeout=5).json()["data"]
|
||||||
except (r.RequestException, ValueError, KeyError):
|
except (r.RequestException, ValueError, KeyError):
|
||||||
abort(404)
|
abort(404)
|
||||||
img["id"] = str(img_id).zfill(2)
|
img["id"] = str(img_id).zfill(2)
|
||||||
|
|||||||
@@ -11,6 +11,16 @@ IMG_MIN, IMG_MAX = 1, 16
|
|||||||
# Поля профиля, которые вообще могут существовать
|
# Поля профиля, которые вообще могут существовать
|
||||||
ALLOWED_FIELDS = {"password", "first_name", "second_name", "email", "msg"}
|
ALLOWED_FIELDS = {"password", "first_name", "second_name", "email", "msg"}
|
||||||
|
|
||||||
|
# PATCH V4: database без auth. Даже если :38002 снова опубликуют на 0.0.0.0,
|
||||||
|
# без этого заголовка (его знают только logic/content) чужие запросы отсекаются.
|
||||||
|
INTERNAL = os.environ.get("RUSGRAM_INTERNAL", "e9b2f6d14a8c7035e1d0a6b8c4f2e7a9")
|
||||||
|
|
||||||
|
|
||||||
|
def require_internal():
|
||||||
|
if request.headers.get("X-Internal") != INTERNAL:
|
||||||
|
return make_response({"status": "forbidden"}, 403)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def safe_username(username):
|
def safe_username(username):
|
||||||
"""Имя юзера становится именем файла в db/users/ — режем всё, что уводит
|
"""Имя юзера становится именем файла в db/users/ — режем всё, что уводит
|
||||||
@@ -30,6 +40,9 @@ def user_path(username):
|
|||||||
|
|
||||||
@app.route('/users/<string:username>/<string:field>', methods=["GET", "POST"])
|
@app.route('/users/<string:username>/<string:field>', methods=["GET", "POST"])
|
||||||
def users(username, field):
|
def users(username, field):
|
||||||
|
denied = require_internal()
|
||||||
|
if denied is not None:
|
||||||
|
return denied
|
||||||
if not safe_username(username) or field not in ALLOWED_FIELDS:
|
if not safe_username(username) or field not in ALLOWED_FIELDS:
|
||||||
return make_response({"status": "bad_request"}, 400)
|
return make_response({"status": "bad_request"}, 400)
|
||||||
path = user_path(username)
|
path = user_path(username)
|
||||||
@@ -82,6 +95,9 @@ def users(username, field):
|
|||||||
|
|
||||||
@app.route('/images/<string:id>', methods=["GET", "POST"])
|
@app.route('/images/<string:id>', methods=["GET", "POST"])
|
||||||
def images(id):
|
def images(id):
|
||||||
|
denied = require_internal()
|
||||||
|
if denied is not None:
|
||||||
|
return denied
|
||||||
# PATCH V8: раньше любой нечисловой/несуществующий id давал 500
|
# PATCH V8: раньше любой нечисловой/несуществующий id давал 500
|
||||||
try:
|
try:
|
||||||
img_id = int(id)
|
img_id = int(id)
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "38000:5000"
|
- "38000:5000"
|
||||||
|
|
||||||
|
# Внутренние ноды НЕ публикуем на хост вообще.
|
||||||
|
# Атака #244475: GET :38002/images/16 без auth → флаги из comments.
|
||||||
|
# Даже 127.0.0.1:38002 опасно, если на боксе крутят туннели/агентов.
|
||||||
static:
|
static:
|
||||||
build: ./static
|
build: ./static
|
||||||
image: git.itqdev.xyz/4x10m/rusgram-static:${IMAGE_TAG:-latest}
|
image: git.itqdev.xyz/4x10m/rusgram-static:${IMAGE_TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
expose:
|
||||||
- "127.0.0.1:38001:5001"
|
- "5001"
|
||||||
volumes:
|
volumes:
|
||||||
- "./website:/app/files"
|
- "./website:/app/files"
|
||||||
|
|
||||||
@@ -19,8 +22,10 @@ services:
|
|||||||
build: ./database
|
build: ./database
|
||||||
image: git.itqdev.xyz/4x10m/rusgram-database:${IMAGE_TAG:-latest}
|
image: git.itqdev.xyz/4x10m/rusgram-database:${IMAGE_TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
environment:
|
||||||
- "127.0.0.1:38002:5002"
|
RUSGRAM_INTERNAL: "e9b2f6d14a8c7035e1d0a6b8c4f2e7a9"
|
||||||
|
expose:
|
||||||
|
- "5002"
|
||||||
volumes:
|
volumes:
|
||||||
- "./website:/app/files"
|
- "./website:/app/files"
|
||||||
|
|
||||||
@@ -28,14 +33,18 @@ services:
|
|||||||
build: ./logic
|
build: ./logic
|
||||||
image: git.itqdev.xyz/4x10m/rusgram-logic:${IMAGE_TAG:-latest}
|
image: git.itqdev.xyz/4x10m/rusgram-logic:${IMAGE_TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
environment:
|
||||||
- "127.0.0.1:38003:5003"
|
RUSGRAM_INTERNAL: "e9b2f6d14a8c7035e1d0a6b8c4f2e7a9"
|
||||||
|
expose:
|
||||||
|
- "5003"
|
||||||
|
|
||||||
content:
|
content:
|
||||||
build: ./content
|
build: ./content
|
||||||
image: git.itqdev.xyz/4x10m/rusgram-content:${IMAGE_TAG:-latest}
|
image: git.itqdev.xyz/4x10m/rusgram-content:${IMAGE_TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
environment:
|
||||||
- "127.0.0.1:38004:5004"
|
RUSGRAM_INTERNAL: "e9b2f6d14a8c7035e1d0a6b8c4f2e7a9"
|
||||||
|
expose:
|
||||||
|
- "5004"
|
||||||
volumes:
|
volumes:
|
||||||
- "./website:/app/files"
|
- "./website:/app/files"
|
||||||
|
|||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Жёстко закрыть 38001-38004 на вулнбоксе + пересобрать rusgram.
|
||||||
|
# Docker публикует порты МИМО цепочки INPUT — поэтому DOCKER-USER обязателен.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo "[*] rebuild + up"
|
||||||
|
docker compose down --remove-orphans || true
|
||||||
|
docker compose up -d --build --force-recreate
|
||||||
|
|
||||||
|
echo "[*] firewall: DROP 38001-38004 (INPUT + DOCKER-USER)"
|
||||||
|
for chain in INPUT DOCKER-USER; do
|
||||||
|
# идемпотентно: сначала снести старые наши правила, потом поставить
|
||||||
|
while iptables -C "$chain" -p tcp --dport 38001:38004 -j DROP 2>/dev/null; do
|
||||||
|
iptables -D "$chain" -p tcp --dport 38001:38004 -j DROP || true
|
||||||
|
done
|
||||||
|
iptables -I "$chain" -p tcp --dport 38001:38004 -j DROP
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[*] published ports now:"
|
||||||
|
docker compose ps
|
||||||
|
ss -ltn | grep -E '3800[0-4]' || echo "(нет слушателей 38001-38004 — ок)"
|
||||||
|
|
||||||
|
echo "[*] self-check: :38002 снаружи должен быть мёртв"
|
||||||
|
echo " curl -s -m 2 http://127.0.0.1:38002/images/1 → connection refused"
|
||||||
|
echo " curl -s -m 2 http://127.0.0.1:38000/login.html → 200"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
from flask import Flask, make_response, request, redirect
|
from flask import Flask, make_response, request, redirect
|
||||||
|
import os
|
||||||
import requests as r
|
import requests as r
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
database_node_port = 5002
|
database_node_port = 5002
|
||||||
@@ -7,6 +8,8 @@ logic_node_port = 5003
|
|||||||
database_node = "database:" + str(database_node_port)
|
database_node = "database:" + str(database_node_port)
|
||||||
|
|
||||||
IMG_MIN, IMG_MAX = 1, 16
|
IMG_MIN, IMG_MAX = 1, 16
|
||||||
|
INTERNAL = os.environ.get("RUSGRAM_INTERNAL", "e9b2f6d14a8c7035e1d0a6b8c4f2e7a9")
|
||||||
|
DBH = {"X-Internal": INTERNAL}
|
||||||
|
|
||||||
|
|
||||||
def bad_request(status="error", code=400):
|
def bad_request(status="error", code=400):
|
||||||
@@ -31,9 +34,16 @@ def valid_username(username):
|
|||||||
return False # имя юзера = имя файла в db/users/
|
return False # имя юзера = имя файла в db/users/
|
||||||
if username.lower().endswith((".html", ".htm")):
|
if username.lower().endswith((".html", ".htm")):
|
||||||
return False # имя, которое content мог бы отрендерить как шаблон
|
return False # имя, которое content мог бы отрендерить как шаблон
|
||||||
|
if "{{" in username or "{%" in username or "{#" in username:
|
||||||
|
return False
|
||||||
return True
|
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():
|
def current_user():
|
||||||
"""Возвращает имя авторизованного юзера или None. Не бросает исключений."""
|
"""Возвращает имя авторизованного юзера или None. Не бросает исключений."""
|
||||||
session = request.cookies.get("session")
|
session = request.cookies.get("session")
|
||||||
@@ -46,7 +56,8 @@ def current_user():
|
|||||||
if not username:
|
if not username:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
resp = r.get(f"http://{database_node}/users/{username}/password", timeout=5)
|
resp = r.get(f"http://{database_node}/users/{username}/password",
|
||||||
|
headers=DBH, timeout=5)
|
||||||
if resp.status_code != 200: # PATCH V8: несуществующий юзер -> KeyError -> 500
|
if resp.status_code != 200: # PATCH V8: несуществующий юзер -> KeyError -> 500
|
||||||
return None
|
return None
|
||||||
if resp.json().get("data") != password:
|
if resp.json().get("data") != password:
|
||||||
@@ -70,7 +81,8 @@ def login():
|
|||||||
if not isinstance(username, str) or not isinstance(password, str):
|
if not isinstance(username, str) or not isinstance(password, str):
|
||||||
return bad_request()
|
return bad_request()
|
||||||
try:
|
try:
|
||||||
resp = r.get(f"http://{database_node}/users/{username}/password", timeout=5)
|
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
|
correct_password = resp.json()["data"] if resp.status_code == 200 else None
|
||||||
except (r.RequestException, ValueError, KeyError):
|
except (r.RequestException, ValueError, KeyError):
|
||||||
return bad_request()
|
return bad_request()
|
||||||
@@ -96,7 +108,8 @@ def register():
|
|||||||
# его пароль (остальные поля, включая флаг, сохранялись) => полный захват
|
# его пароль (остальные поля, включая флаг, сохранялись) => полный захват
|
||||||
# чужого аккаунта без единого запроса на аутентификацию.
|
# чужого аккаунта без единого запроса на аутентификацию.
|
||||||
try:
|
try:
|
||||||
exists = r.get(f"http://{database_node}/users/{username}/password", timeout=5)
|
exists = r.get(f"http://{database_node}/users/{username}/password",
|
||||||
|
headers=DBH, timeout=5)
|
||||||
if exists.status_code == 200:
|
if exists.status_code == 200:
|
||||||
return make_response({"status": "user_exists"}, 409)
|
return make_response({"status": "user_exists"}, 409)
|
||||||
except r.RequestException:
|
except r.RequestException:
|
||||||
@@ -104,7 +117,7 @@ def register():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
resp = r.post(f"http://{database_node}/users/{username}/password",
|
resp = r.post(f"http://{database_node}/users/{username}/password",
|
||||||
json={"value": password}, timeout=5)
|
json={"value": password}, headers=DBH, timeout=5)
|
||||||
except r.RequestException:
|
except r.RequestException:
|
||||||
return bad_request()
|
return bad_request()
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
@@ -123,12 +136,12 @@ def update_personal():
|
|||||||
if data is None:
|
if data is None:
|
||||||
return bad_request()
|
return bad_request()
|
||||||
for field in ("first_name", "second_name", "email"):
|
for field in ("first_name", "second_name", "email"):
|
||||||
if field not in data or not isinstance(data[field], str):
|
if field not in data or not clean_value(data[field]):
|
||||||
return bad_request()
|
return bad_request()
|
||||||
try:
|
try:
|
||||||
for field in ("first_name", "second_name", "email"):
|
for field in ("first_name", "second_name", "email"):
|
||||||
r.post(f"http://{database_node}/users/{username}/{field}",
|
r.post(f"http://{database_node}/users/{username}/{field}",
|
||||||
json={"value": data[field]}, timeout=5)
|
json={"value": data[field]}, headers=DBH, timeout=5)
|
||||||
except r.RequestException:
|
except r.RequestException:
|
||||||
return bad_request()
|
return bad_request()
|
||||||
return make_response({"status": "ok"}, 200)
|
return make_response({"status": "ok"}, 200)
|
||||||
@@ -140,11 +153,11 @@ def send_msg():
|
|||||||
if username is None:
|
if username is None:
|
||||||
return unauthorized()
|
return unauthorized()
|
||||||
data = json_body()
|
data = json_body()
|
||||||
if data is None or not isinstance(data.get("msg"), str):
|
if data is None or not clean_value(data.get("msg")):
|
||||||
return bad_request()
|
return bad_request()
|
||||||
try:
|
try:
|
||||||
r.post(f"http://{database_node}/users/{username}/msg",
|
r.post(f"http://{database_node}/users/{username}/msg",
|
||||||
json={"value": data["msg"]}, timeout=5)
|
json={"value": data["msg"]}, headers=DBH, timeout=5)
|
||||||
except r.RequestException:
|
except r.RequestException:
|
||||||
return bad_request()
|
return bad_request()
|
||||||
return make_response({"status": "ok"}, 200)
|
return make_response({"status": "ok"}, 200)
|
||||||
@@ -156,7 +169,7 @@ def post_comment():
|
|||||||
if username is None:
|
if username is None:
|
||||||
return unauthorized()
|
return unauthorized()
|
||||||
data = json_body()
|
data = json_body()
|
||||||
if data is None or not isinstance(data.get("msg"), str):
|
if data is None or not clean_value(data.get("msg")):
|
||||||
return bad_request()
|
return bad_request()
|
||||||
# PATCH V8: img_id раньше уходил в URL внутреннего запроса как есть
|
# PATCH V8: img_id раньше уходил в URL внутреннего запроса как есть
|
||||||
try:
|
try:
|
||||||
@@ -167,7 +180,7 @@ def post_comment():
|
|||||||
return bad_request()
|
return bad_request()
|
||||||
try:
|
try:
|
||||||
r.post(f"http://{database_node}/images/{img_id}",
|
r.post(f"http://{database_node}/images/{img_id}",
|
||||||
json={"value": [username, data["msg"]]}, timeout=5)
|
json={"value": [username, data["msg"]]}, headers=DBH, timeout=5)
|
||||||
except r.RequestException:
|
except r.RequestException:
|
||||||
return bad_request()
|
return bad_request()
|
||||||
return make_response({"status": "ok"}, 200)
|
return make_response({"status": "ok"}, 200)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
from flask import Flask, request, redirect
|
from flask import Flask, request, redirect, abort
|
||||||
import requests as r
|
import requests as r
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
proxy_node_port = 5000
|
proxy_node_port = 5000
|
||||||
@@ -10,29 +10,49 @@ static_node = "static:" + str(static_node_port)
|
|||||||
logic_node = "logic:" + str(logic_node_port)
|
logic_node = "logic:" + str(logic_node_port)
|
||||||
content_node = "content:" + str(content_node_port)
|
content_node = "content:" + str(content_node_port)
|
||||||
|
|
||||||
|
# Только реальные страницы. /db/users/pwn.html больше не уходит в content (V1).
|
||||||
|
ALLOWED_HTML = {"index.html", "login.html", "register.html", "specific.html"}
|
||||||
|
ALLOWED_API = {"login", "register", "update_personal", "send_msg", "post_comment"}
|
||||||
|
STATIC_EXT = {"css", "png", "jpg", "ico", "js", "otf", "eot", "svg", "ttf", "woff", "woff2"}
|
||||||
|
|
||||||
|
|
||||||
|
def safe_path(path: str):
|
||||||
|
if not path or ".." in path or "\\" in path or "\x00" in path:
|
||||||
|
return False
|
||||||
|
if path.startswith("db/") or "/db/" in path:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@app.route('/', defaults={'path': 'index.html'})
|
@app.route('/', defaults={'path': 'index.html'})
|
||||||
@app.route('/<path:path>', methods=["GET", "POST"])
|
@app.route('/<path:path>', methods=["GET", "POST"])
|
||||||
def catch_all(path: str):
|
def catch_all(path: str):
|
||||||
print(path)
|
if not safe_path(path):
|
||||||
|
abort(404)
|
||||||
ext = path.split('.')[-1]
|
ext = path.split('.')[-1]
|
||||||
if ext == "html":
|
if ext == "html":
|
||||||
resp = r.get(f"http://{content_node}/{path}?{request.full_path.split('?')[1]}", headers=request.headers)
|
if path not in ALLOWED_HTML:
|
||||||
|
abort(404)
|
||||||
|
qs = request.full_path.split('?')[1] if '?' in request.full_path else ''
|
||||||
|
resp = r.get(f"http://{content_node}/{path}?{qs}", headers=request.headers,
|
||||||
|
timeout=10)
|
||||||
return resp.content, resp.status_code, resp.headers.items()
|
return resp.content, resp.status_code, resp.headers.items()
|
||||||
if ext in ["css", "png", "jpg", "ico", "js", "otf", "eot", "svg", "ttf", "woff", "woff2"]:
|
if ext in STATIC_EXT:
|
||||||
resp = r.get(f"http://{static_node}/{path}", stream=True)
|
resp = r.get(f"http://{static_node}/{path}", stream=True, timeout=5)
|
||||||
headers = [("Cache-Control", "max-age=36000;"), ("Content-Type", resp.headers["Content-Type"])]
|
headers = [("Cache-Control", "max-age=36000;"), ("Content-Type", resp.headers["Content-Type"])]
|
||||||
return resp.content, resp.status_code, headers
|
return resp.content, resp.status_code, headers
|
||||||
if path.startswith("api/"):
|
if path.startswith("api/"):
|
||||||
|
action = path[4:].split("/", 1)[0]
|
||||||
|
if action not in ALLOWED_API:
|
||||||
|
abort(404)
|
||||||
if request.method.lower() not in ("get", "post"):
|
if request.method.lower() not in ("get", "post"):
|
||||||
return redirect("/", code=302)
|
return redirect("/", code=302)
|
||||||
passcall = getattr(r, request.method.lower())
|
passcall = getattr(r, request.method.lower())
|
||||||
# PATCH V3: allow_redirects=False. requests по умолчанию ХОДИТ по Location,
|
# PATCH V3: allow_redirects=False. requests по умолчанию ХОДИТ по Location,
|
||||||
# который отдаёт logic, а Location раньше строился из заголовка Origin =>
|
# который отдаёт logic, а Location раньше строился из заголовка Origin =>
|
||||||
# proxy делал запрос во внутреннюю сеть за атакующего (SSRF).
|
# proxy делал запрос во внутреннюю сеть за атакующего (SSRF).
|
||||||
# Редирект теперь отдаём клиенту, а не отрабатываем сами.
|
|
||||||
resp = passcall(f"http://{logic_node}/{path[4:]}", json=request.get_json(silent=True),
|
resp = passcall(f"http://{logic_node}/{path[4:]}", json=request.get_json(silent=True),
|
||||||
headers=request.headers, allow_redirects=False)
|
headers=request.headers, allow_redirects=False, timeout=8)
|
||||||
return resp.content, resp.status_code, resp.headers.items()
|
return resp.content, resp.status_code, resp.headers.items()
|
||||||
return redirect("/", code=302)
|
return redirect("/", code=302)
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,35 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
from flask import Flask, send_file
|
from flask import Flask, send_file, abort
|
||||||
|
import os
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
static_node_port = 5001
|
static_node_port = 5001
|
||||||
|
|
||||||
|
|
||||||
|
def safe_name(name):
|
||||||
|
# <name> normally has no slashes, but %2f / .. still show up decoded.
|
||||||
|
if not name or name != os.path.basename(name) or ".." in name or "\x00" in name:
|
||||||
|
abort(404)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
@app.route('/img/<name>')
|
@app.route('/img/<name>')
|
||||||
def img(name):
|
def img(name):
|
||||||
return send_file(f'files/static/img/{name}')
|
return send_file(f'files/static/img/{safe_name(name)}')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/css/<name>')
|
@app.route('/css/<name>')
|
||||||
def css(name):
|
def css(name):
|
||||||
return send_file(f'files/static/css/{name}')
|
return send_file(f'files/static/css/{safe_name(name)}')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/fonts/<name>')
|
@app.route('/fonts/<name>')
|
||||||
def fonts(name):
|
def fonts(name):
|
||||||
return send_file(f'files/static/fonts/{name}')
|
return send_file(f'files/static/fonts/{safe_name(name)}')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/js/<name>')
|
@app.route('/js/<name>')
|
||||||
def js(name):
|
def js(name):
|
||||||
return send_file(f'files/static/js/{name}')
|
return send_file(f'files/static/js/{safe_name(name)}')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in New Issue
Block a user