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:
@@ -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] = {}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
))
|
||||
|
||||
|
||||
@@ -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] = {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user