Security audit: fix IDOR, add rate limiting, HMAC auth, token hashing, XSS sanitization

Critical: Add ownership checks to all order routes (IDOR fix).
High: Redis rate limiting on auth endpoints, HMAC-signed internal
service calls replacing header-presence-only checks, nh3 HTML
sanitization on ghost_sync and product import, internal auth on
market API endpoints, SHA-256 hashed OAuth grant/code tokens.
Medium: SECRET_KEY production guard, AP signature enforcement,
is_admin param removal, cart_sid validation, SSRF protection on
remote actor fetch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 13:30:27 +00:00
parent 404449fcab
commit c015f3f02f
27 changed files with 607 additions and 33 deletions

View File

@@ -18,6 +18,9 @@ def register() -> Blueprint:
async def _require_action_header():
if not request.headers.get(ACTION_HEADER):
return jsonify({"error": "forbidden"}), 403
from shared.infrastructure.internal_auth import validate_internal_request
if not validate_internal_request():
return jsonify({"error": "forbidden"}), 403
_handlers: dict[str, object] = {}

View File

@@ -5,6 +5,7 @@ from datetime import datetime, timezone
from decimal import Decimal
from typing import Any, Dict, List, Tuple, Iterable, Optional
import nh3
from quart import Blueprint, request, jsonify, g
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -29,10 +30,18 @@ from models.market import (
from shared.browser.app.redis_cacher import clear_cache
from shared.browser.app.csrf import csrf_exempt
from shared.infrastructure.internal_auth import validate_internal_request
products_api = Blueprint("products_api", __name__, url_prefix="/api/products")
@products_api.before_request
async def _require_internal_auth():
"""All product API endpoints require HMAC-signed internal requests."""
if not validate_internal_request():
return jsonify({"error": "forbidden"}), 403
# ---- Comparison config (matches your schema) --------------------------------
PRODUCT_FIELDS: List[str] = [
@@ -219,9 +228,35 @@ def _deep_equal(a: Dict[str, Any], b: Dict[str, Any]) -> bool:
# ---- Mutation helpers -------------------------------------------------------
_PRODUCT_HTML_FIELDS = {"description_html"}
_SANITIZE_TAGS = {
"a", "b", "blockquote", "br", "code", "div", "em", "h1", "h2", "h3",
"h4", "h5", "h6", "hr", "i", "img", "li", "ol", "p", "pre", "span",
"strong", "sub", "sup", "table", "tbody", "td", "th", "thead", "tr",
"ul", "figure", "figcaption",
}
_SANITIZE_ATTRS = {
"*": {"class", "id"},
"a": {"href", "title", "target", "rel"},
"img": {"src", "alt", "title", "width", "height", "loading"},
"td": {"colspan", "rowspan"},
"th": {"colspan", "rowspan"},
}
def _sanitize_product_html(value: Any) -> Any:
if isinstance(value, str) and value:
return nh3.clean(value, tags=_SANITIZE_TAGS, attributes=_SANITIZE_ATTRS)
return value
def _apply_product_fields(p: Product, payload: Dict[str, Any]) -> None:
for f in PRODUCT_FIELDS:
setattr(p, f, payload.get(f))
val = payload.get(f)
if f in _PRODUCT_HTML_FIELDS:
val = _sanitize_product_html(val)
setattr(p, f, val)
p.updated_at = _now_utc()
def _replace_children(p: Product, payload: Dict[str, Any]) -> None:
@@ -239,7 +274,7 @@ def _replace_children(p: Product, payload: Dict[str, Any]) -> None:
for row in payload.get("sections") or []:
p.sections.append(ProductSection(
title=row.get("title") or "",
html=row.get("html") or "",
html=_sanitize_product_html(row.get("html") or ""),
created_at=_now_utc(), updated_at=_now_utc(),
))

View File

@@ -19,6 +19,9 @@ def register() -> Blueprint:
async def _require_data_header():
if not request.headers.get(DATA_HEADER):
return jsonify({"error": "forbidden"}), 403
from shared.infrastructure.internal_auth import validate_internal_request
if not validate_internal_request():
return jsonify({"error": "forbidden"}), 403
_handlers: dict[str, object] = {}