API/BOLA: почему две роли важнее ещё одного сканера
Схема лабораторной проверки: от проблемы к доказательству, решению и ретесту.
Коротко: Если нет двух тестовых аккаунтов и inventory endpoint-ов, отчёт честно должен писать limitation. Если есть роли, HAR/OpenAPI и read-only replay, SecMon может строить role matrix и отличать публичный endpoint от реального обхода авторизации.

Проблема

Команды часто проверяют API как набор URL, а не как модель владения объектами. В результате endpoint выглядит защищенным на уровне функции, но пропускает объект другого пользователя.

Сырые сканеры почти не понимают бизнес-смысл: они видят параметры id, userId, accountId, но не знают, кому объект принадлежит и какая роль должна его видеть.

Решение

Начинать нужно с inventory: HAR, OpenAPI, роли, object identifiers и список sensitive flows. После этого read-only replay сравнивает ответы роли A и роли B без изменения состояния.

Finding появляется только если есть role-differential evidence: статус, редирект, content-type, размер ответа, hash и безопасно редактированный фрагмент доказательства.

Что проверить руками

  • Собрать HAR от двух тестовых аккаунтов: owner и viewer.
  • Отметить endpoint-ы account, profile, billing, upload, admin, invite.
  • Проверить только GET/HEAD/OPTIONS и зафиксировать, где роль B видит объект роли A.
  • Не сохранять raw body с PII в отчёт, а хранить hash и редактированное evidence.

Безопасный пример кода

Read-only inventory extractor из HAR. Пример рассчитан на owned/lab-среду и не выполняет вредоносных действий.

import json
from pathlib import Path
from urllib.parse import urlsplit

OBJECT_KEYS = ("id", "userId", "accountId", "profileId", "invoiceId", "orderId")

def collect_operations(har_path: str) -> list[dict]:
    har = json.loads(Path(har_path).read_text(encoding="utf-8"))
    operations = []
    for item in har.get("log", {}).get("entries", []):
        req = item.get("request", {})
        method = req.get("method", "GET").upper()
        url = req.get("url", "")
        parsed = urlsplit(url)
        params = [p.get("name", "") for p in req.get("queryString", [])]
        object_params = [name for name in params if name in OBJECT_KEYS or name.lower().endswith("id")]
        operations.append({
            "method": method,
            "host": parsed.netloc,
            "path": parsed.path,
            "read_only": method in {"GET", "HEAD", "OPTIONS"},
            "object_params": sorted(set(object_params)),
        })
    return operations

for op in collect_operations("owned-test-session.har"):
    if op["read_only"] and op["object_params"]:
        print(op)

Как это должно попасть в отчёт

  • Role Matrix: owner/viewer/admin по каждому sensitive endpoint.
  • Coverage: authenticated_not_configured, single_role_only, stateful_executed.
  • Retest: тот же read-only запрос от роли B должен возвращать 403/404 или объект роли B.
Этическая рамка: материал предназначен для defensive security, аудита собственных систем, обучения и подготовки remediation. Он не содержит инструкций для несанкционированного доступа, закрепления, обхода защит или эксфильтрации.