Security: deep /api/health (DB+MinIO probes, 503 on failure) + /api/live; rate-limited public forms with honeypots; real client IP behind Traefik (--proxy-headers); TipTap URL sanitization on write (nh3); non-root containers + slim .dockerignore; prod MinIO console bound to 127.0.0.1; HSTS/nosniff on TLS routers; JWT 8h; no default credentials in code. Reliability: Alembic migrations (create_all baseline); graceful shutdown (exec uvicorn); hardened backups (checksums, sanity check, freshness gauge, optional rclone off-site) + restore.sh + hourly db-backup sidecar (ops profile); stack.sh ops tooling (deploy/logs/health/backup/restore/monitoring); .gitattributes LF for shell scripts. Orchestration: json-file log rotation, pinned images, per-service memory limits, edge/internal networks, Traefik ping/accesslog/metrics + flood limiter, conditional minio-setup, pgvector db image, one-shot db-init (umami + aramesh_chatbot databases). Chatbot: unified from site-chatbot branch (RAG + pgvector, SSE, ChatWidget). Monitoring: Prometheus/Alertmanager/Loki/Promtail/Grafana + cAdvisor, node/postgres/blackbox exporters, provisioned dashboards and 10 alert rules; alerts delivered to Bale (tapi.bale.ai — Telegram-compatible). Analytics: Umami on shared Postgres, build-time tracker injection, custom events (contact/newsletter/phone/chat/lang) via frontend/src/analytics.ts. Hygiene: CSV export for contact messages and subscribers; upload image optimization (Pillow); CI script + GitHub-compatible workflow; audit.sh; gen-env.sh for strong credentials.
59 lines
2.3 KiB
Python
59 lines
2.3 KiB
Python
"""پاکسازی ورودیهای غنی در زمان ذخیره.
|
|
|
|
بدنه مقالهها سند TipTap (ProseMirror) است و فرانتاند همان را با
|
|
renderToHTMLString به HTML تبدیل و مستقیم تزریق میکند؛ متن گرهها در آن
|
|
مسیر escape میشود، اما آدرسهای href/src عیناً وارد HTML میشوند — پس
|
|
اسکیمهای خطرناک (javascript: و data: و ...) باید در سمت سرور حذف شوند.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from urllib.parse import urlparse
|
|
|
|
# اسکیمهای مجاز برای href/src — مسیرهای نسبی و لنگر هم بیاسکیماند و میگذرند
|
|
SAFE_URL_SCHEMES = {"http", "https", "mailto", "tel"}
|
|
URL_ATTRS = ("href", "src")
|
|
|
|
|
|
def _clean_url(value: Any) -> str | None:
|
|
"""آدرس امن را برمیگرداند؛ در غیر این صورت None (مشخصه حذف میشود)."""
|
|
if not isinstance(value, str):
|
|
return value if value is not None else None
|
|
stripped = value.strip().replace("\n", "").replace("\r", "").replace("\t", "")
|
|
if not stripped:
|
|
return None
|
|
parsed = urlparse(stripped)
|
|
if parsed.scheme and parsed.scheme.lower() not in SAFE_URL_SCHEMES:
|
|
return None
|
|
return stripped
|
|
|
|
|
|
def _walk(node: Any) -> Any:
|
|
if isinstance(node, list):
|
|
return [_walk(item) for item in node]
|
|
if not isinstance(node, dict):
|
|
return node
|
|
cleaned: dict[str, Any] = {}
|
|
for key, value in node.items():
|
|
if key == "attrs" and isinstance(value, dict):
|
|
attrs = dict(value)
|
|
for attr in URL_ATTRS:
|
|
if attr in attrs:
|
|
cleaned_url = _clean_url(attrs[attr])
|
|
if cleaned_url is None:
|
|
attrs.pop(attr, None)
|
|
else:
|
|
attrs[attr] = cleaned_url
|
|
cleaned[key] = attrs
|
|
else:
|
|
cleaned[key] = _walk(value)
|
|
return cleaned
|
|
|
|
|
|
def sanitize_doc(doc: Any) -> Any:
|
|
"""سند TipTap را درجا پاک میکند (سایر ساختارها دستنخورده برمیگردند)."""
|
|
if not isinstance(doc, dict):
|
|
return doc
|
|
return _walk(doc)
|