
Находит маршруты API с более слабой авторизацией, чем у их соседних маршрутов. Восстановлен CVE-2026-45316 из исходного кода. Включает отрицательные результаты.
Находит API-маршруты, которые несут более слабый контроль авторизации, чем их соседние маршруты.
Он нашёл CVE-2026-45316 в Open WebUI по одному только исходному коду, без сведений из бюллетеней безопасности:
[MISMATCH] POST /notes/{id}/pin (notes.py:pin_note_by_id)
3/3 comparable write operations on note require has_access(write);
this route requires only has_access(read)
POST /notes/{id}/update has_access(write)
POST /notes/{id}/access/update has_access(write)
DELETE /notes/{id}/delete has_access(write)
На 2 568 маршрутах в пяти продакшен-кодовых базах он выдал 4 находки. Две из них были реальными. Этот README объясняет обе цифры.
Большой класс ошибок авторизации — это не сломанная проверка. Это отсутствующая или ослабленная проверка на маршруте, у которого все соседние маршруты выполнены правильно.
Portainer авторизовал четыре соседних endpoint'а шаблонов, но не пятый. Signal K ограничивал частоту HTTP-логина, но не WebSocket-логина. Маршрут закрепления заметки в Open WebUI изменял заметку, проверяя разрешение на чтение, тогда как все остальные операции изменения заметки проверяли запись.
Всегда одна и та же форма:
✓ ✓ ✓ ✓ ✗
Репозиторий уже содержит правило. Один маршрут его нарушил. Поэтому восстановите правило из кода и сообщите об исключении. Никаких policy-файлов, конфигов и аннотаций. Доказательная база — сами соседние маршруты.
python3 enforcement_coverage.py /path/to/repo
python3 enforcement_coverage.py /path/to/repo --density
python3 enforcement_coverage.py /path/to/repo --json
Python 3.10+, без зависимостей. Только FastAPI.
Вердикты: MISSING, MISMATCH, PRESERVED, UNKNOWN. UNKNOWN воздерживается и никогда не сообщается.
Извлечение. Разрешает контроли из сигнатуры Depends(), декоратора dependencies=[], зависимостей уровня роутера, тел функций, функций-обёрток и списков классов разрешений.
Извлечение из тела функции критически важно. В Open WebUI 216 из 608 обработчиков несут решающую проверку внутри функции:
if user.role != 'admin' and not await AccessGrants.has_access(
user_id=user.id, resource_type='note',
resource_id=note.id, permission='write', db=db,
):
raise HTTPException(status_code=403)
Класс операции определяется тем, что обработчик делает с ресурсом, а не HTTP-глаголом. POST /notes/{id}/chat — это POST, который читает заметку.
Классификация словаря. Два значения в одном семействе контролей не обязательно образуют шкалу силы:
has_access {read, write} LEVEL
ensure_flow_permission {create,delete,execute,read,write} ACTION
has_permission {features.notes, workspace.tools} SCOPE
Только словари LEVEL поддерживают сравнение силы. Сравнение FlowAction.CREATE с доминирующим WRITE давало шесть ложных срабатываний в Langflow, пока это не исправили.
Фильтр направления. Сообщается только об отклонениях в сторону менее ограничивающего контроля. Более строгий, чем прецедент, контроль — это не уязвимость.
Два истинно положительных результата.
POST /notes/{id}/pin в Open WebUI — CVE-2026-45316.
Ещё одна находка — асимметрия разрешений в Netflix Dispatch: POST /{incident_id}/resources использует IncidentViewPermission, тогда как шесть соседних операций записи используют IncidentEditPermission. IncidentViewPermission возвращает True для любого неограниченного инцидента; IncidentEditPermission требует прав администратора, командира или репортёра. Обработчик ставит в очередь создание тикета и группы без дополнительной проверки.
Не сообщено, потому что сообщить некуда: репозиторий был заархивирован Netflix 3 сентября 2025 года и доступен только для чтения, в нём нет SECURITY.md, приватное сообщение об уязвимостях отключено для заархивированных репозиториев, а Dispatch явно указан как вне зоны действия программы bug bounty Netflix. Публикация здесь — единственный оставшийся канал раскрытия. Уязвимость имеет низкую степень серьёзности — она требует аутентифицированного участника организации и затрагивает только неограниченные инциденты — а проект больше не поддерживается.
Два ложных срабатывания. POST /tools/{id}/valves/user/update записывает собственные настройки клапанов пользователя и обоснованно требует только чтения инструмента — цель изменения является другой сущностью, чем субъект авторизации. А также маршрут базы знаний в Langflow, где соседняя защита не требуется.
Это полезная часть.
У Danswer 95,9% покрытия контролей и ноль находок при 80,7% UNKNOWN. Его словарь — require_permission('basic_access'), ('manage_connectors') — это пространство имён возможностей, а не шкала силы. Нельзя сказать, что manage_connectors слабее, чем read_connectors.
Находки предсказывает вызов разрешения с областью действия ресурса и упорядоченным аргументом силы, например has_access(resource, read|write). Такое было в одной кодовой базе из пяти.
Пять идиом в пяти репозиториях. Для каждой требовалась работа экстрактора, прежде чем анализ вообще мог запуститься.
В любом репозитории и при любой конфигурации. Большинство маршрутов не принадлежат соседнему семейству из трёх и более маршрутов с согласованным контролем.
Ни один из них не был предвиден заранее.
Depends()Дефект 13 — самый интересный. Маршруты рекомендации тегов в Dispatch не имеют CaseViewPermission, который есть у соседних маршрутов. Но это разрешение возвращает True для любого неограниченного кейса, а сервис уже проверяет visibility == restricted инлайн. Пути эквивалентны. Для обнаружения этого нужен анализ семантической эквивалентности, а не структуры.
Механизм работает. Он восстановил опубликованный CVE и один несообщённый пробел в авторизации из исходного кода, с проверяемыми наборами доказательств, используя только информацию, доступную до слияния уязвимого коммита.
Выход — примерно одна находка на 1 000 маршрутов, и для этого нужна архитектура, которая была у одной из пяти крупных кодовых баз.
Полезен как инструмент аудита для кодовой базы со словарём разрешений в области ресурсов. На основании этих данных — не универсальный сканер.
Статическое обнаружение уязвимостей контроля доступа путём вывода неявных предположений восходит к USENIX Security 2011. ACMiner извлекал проверки авторизации в промежуточном ПО Android. Semgrep поставляет обнаружение на основе ИИ, нацеленное на отсутствующую авторизацию, и сообщило о точности 61% в одной оценке заказчика. OWASP публикует шпаргалку по регрессионному тестированию авторизации, рекомендуемый подход которой — ведение матрицы Actor × Resource × Action вручную.
Это узкий, детерминированный подход: восстанавливать только то, что доказывают соседние маршруты, в остальном воздерживаться.
Под лицензией MIT.
| репозиторий | маршруты | под контролем | находки |
|---|
| LiteLLM | 808 | 87.1% | 0 |
| Danswer / Onyx | 653 | 95.9% | 0 |
| Open WebUI | 529 | 97.2% | 2 |
| Netflix Dispatch | 291 | 36.1% | 1 |
| Langflow | 287 | 47.4% | 1 |
| репозиторий | идиома |
|---|
| Open WebUI | AccessGrants.has_access(resource_type=, permission=) |
| Langflow | ensure_<resource>_permission(user, Action.X) |
| LiteLLM | role compare on user_api_key_dict.user_role |
| Netflix Dispatch | Depends(PermissionsDependency([CaseEditPermission])) |
| Danswer | require_permission('basic_access') |