Сегодня я решил изучить новую библиотеку Cloudflare OAuth provider, которую, судя по заявлениям, почти полностью написали при помощи LLM Claude компании Anthropic:

Эта библиотека (в том числе и документация по схеме) была по большей мере написана при помощи Claude — ИИ-модели компании Anthropic. Результаты работы Claude были тщательно проверены инженерами Cloudflare, уделившим особое внимание безопасности и соответствию стандартам. В исходный результат было внесено множество улучшений, в основном тоже при помощи промптов Claude (и с проверкой результатов). Промпты модели Claude и созданный ею код можно посмотреть в истории коммитов.

[…]

Подчеркнём, что это не «вайб-кодинг». Каждая строка была тщательно проверена и согласована с соответствующими RFC специалистами в сфере безопасности, уже работавшими в этими RFC. Я пытался подтвердить свой скепсис, но оказалось, что я ошибался.

Я и сам в последнее время достаточно много писал подобным образом код при помощи «агентских» LLM. И я тоже специалист по OAuth: я написал API Security in Action, многие годы был членом OAuth Working Group в IETF и ранее работал техлидом, а затем архитектором безопасности в ведущем поставщике решений OAuth. (Также у меня есть степень PhD в сфере ИИ, полученная в группе изучения интеллектуальных агентов, но ещё до возникновения современного ажиотажа вокруг машинного обучения). Поэтому мне было очень любопытно, что же создала эта модель. И сегодня, сидя на паре совещаний, я решил изучить результаты. Дисклеймер: я лишь вкратце просмотрел код и нашёл несколько багов, а не выполнял полный анализ.

Поначалу код меня достаточно сильно впечатлил. Весь код находится в одном файле, что, судя по моему опыту LLM-кодинга, встречается довольно часто, однако он вполне хорошо структурирован и в нём мало бесполезных комментариев, которые LLM любит вставлять в кодовую базу. В нём есть классы и высокоуровневая организация.

Разработчики проводили тесты, и в них не возникало проблем, но они были ужасно неадекватны тому, чего бы я ожидал от критического сервиса аутентификации. Тестирование каждого пункта, который ДОЛЖЕН и НЕ ДОЛЖЕН быть согласно спецификации — это лишь самый минимум, не говоря уже о множестве сценариев зловредного использования, которые можно придумать, но, насколько я увидел, они полностью отсутствовали. Использовались лишь тесты базовой функциональности. (Вкратце пробежавшись по коду, я бы сказал, что там достаточно много отсутствующих проверок того, что ДОЛЖНО присутствовать в коде, в частности, тестов, связанных с валидацией параметров, которая в текущей реализации проверяется очень поверхностно).

Первым, что привлекло моё внимание, было то, что я называю «YOLO CORS». Такой подход встречается довольно часто: заголовки CORS настраиваются так, что, по сути, отключают политику единого домена почти для всех доменов:

private addCorsHeaders(response: Response, request: Request): Response {
    // Получаем заголовок Origin из запроса
    const origin = request.headers.get('Origin');
 
    // Если заголовок Origin отсутствует, возвращаем исходный ответ
    if (!origin) {
      return response;
    }
 
    // Создаём новый ответ, копирующий все свойства из исходного ответа
    // Благодаря этому ответ становится изменяемым и мы можем модифицировать его заголовки
    const newResponse = new Response(response.body, response);
 
    // Добавляем заголовки CORS
    newResponse.headers.set('Access-Control-Allow-Origin', origin);
    newResponse.headers.set('Access-Control-Allow-Methods', '*');
    // Добавляем Authorization в явном виде, потому что она не включена в * по соображениям безопасности
    newResponse.headers.set('Access-Control-Allow-Headers', 'Authorization, *');
    newResponse.headers.set('Access-Control-Max-Age', '86400'); // 24 часа
 
    return newResponse;
  }

В некоторых сценариях такое поведение приемлемо, и я не изучал подробно, почему они так сделали, но мне это кажется очень подозрительным. Так не стоит делать почти никогда. В данном случае из лога коммитов понятно, что такой подход выбрали люди, а не LLM. По крайней мере, они не включили учётные данные (credentials), так что, вероятно, всевозможные проблемы, к которым это обычно приводит, здесь не возникнут.

К слову, о заголовках: в создаваемых ответах чётко заметно отсутствие стандартных заголовков безопасности. Многие из них неприменимы к API, но некоторые применимы (и часто неожиданным образом). Например, в своей книге я показываю, как применить XSS-уязвимость к JSON API: даже если вы возвращаете правильно сформированный JSON, это не значит, что браузер интерпретирует его именно так. Я не знаком с Cloudflare Workers, так что, возможно, они добавляют заголовки самостоятельно, но я бы, по крайней мере, ожидал заголовок X-Content-Type-Options: nosniff и HTTP Strict Transport Security для защиты используемых токенов носителей.

В коде приняты некоторые достаточно странные решения и есть аспекты, из-за которых я пришёл к выводу, что участвовавшие в его разработке люди на самом деле незнакомы со спецификациями OAuth. Например, этот коммит добавляет поддержку публичных клиентов, но делает это при помощи устаревшего «неявного» (implicit) grant (от которого избавились в OAuth 2.1). Это абсолютно точно не нужно для поддержки публичных клиентов, особенно когда остальная часть кода реализует PKCE и снижает строгость CORS. Из сообщения коммита следует, что разработчики не знали, что нужно для поддержки публичных клиентов, поэтому они обратились к Claude, и она предложила использовать implicit grant, которое скрыто за feature flag, но этот флаг проверяется в совершенно опциональном вспомогательном методе для парсинга запроса, а не в точке выпуска токена.

Ещё один намёк на то, что код писался людьми, незнакомыми с OAuth, заключается в некорректной реализации поддержки Basic auth. Это классический баг в реализациях провайдера OAuth, потому что люди (и, очевидно, LLM) предполагают, что это просто ванильная базовая аутентификация, однако в OAuth есть особенность — сначала выполняется URL-кодирование (потому что кодировки символов слишком запутанные). Аналогично, в коде возникает вторичный баг, если в секрете клиента есть двоеточие (допускаемое специализацией). Не думаю, что обе этих проблемы специфичны для этой конкретной реализации, потому что она всегда генерирует ID и секреты клиентов, а значит, может контролировать формат, но подробно я это не изучал.

Более серьёзный баг заключается в ненадёжности кода, генерирующего ID токенов: он генерирует перекошенный вывод. Это классический баг, возникающий, когда люди наивно пытаются генерировать случайные строки; насколько я понимаю, LLM внёс его в самом первом своём коммите. Не думаю, что его могут использовать злоумышленники: он снижает энтропию токенов, но не настолько сильно, чтобы их можно было взламывать перебором. Но это ставит под сомнение заявление о том. что каждую строку сгенерированного ИИ кода проверяли опытные специалисты в сфере безопасности. Если это действительно так и они это упустили, то эти специалисты слишком доверились компетентности LLM. (Но я не думаю, что это так: согласно истории коммитов, в самый первый день одним разработчиком был внесён 21 коммит непосредственно в основную ветвь; при этом нет ни малейших признаков код-ревью).

Я вкратце просмотрел реализацию шифрования для хранилища токенов. По большей части структура мне понравилось! Она довольно продуманная. Из сообщений коммитов можно увидеть, что структуру разработали живые инженеры, но реализация меня заинтересовала. Здесь стоит воспроизвести сообщение коммита по этой работе, демонстрирующее общение разработчика Claude для реализации нужного кода:

Прошу Claude хранить свойства в зашифрованном виде.

промпт: Мне бы хотелось, чтобы props, хранимые в записях Grant и Token, были зашифрованы. Они должны быть зашифрованы так, чтобы для расшифровки требовался бы валидный токен. Реализовать это немного сложно, потому что в процессе работы возникает множество валидных токенов: есть код авторизации, токены обновления (ротируемые) и токены индивидуального доступа. Мы не хотим многократно шифровать заново props. Нам нужно зашифровать их один раз симметричным ключом, а затем сохранять этот ключ, «обёрнутый» в каждый токен, пока токен валиден. Для реализации всей криптографии используй WebCrypto.

Изначально Claude пошла по неверному пути, благодаря чему я осознал, что забыл важный аспект структуры:

промпт: Я забыл добавить следующее: вспомогательная функция listUserGrants() больше не сможет возвращать props, потому что у неё нет токенов для их расшифровки. Это нормально: props нужно передавать приложению только при авторизованном запросе API. Нам нужно изменить listUserGrants(), чтобы она возвращала более узкое представление grant. Пока она возвращает из хранилища всю запись grant, но на самом деле нам нужно возвращать только id, clientId, userId, scope, metadata и createdAt. Нам не нужно возвращать ID токенов и информацию вызовов кода.

Claude создала красивый код с одним огромным изъяном.

промпт: Возник изъян безопасности в способе обёртывания ключей для токенов: в качестве материала ключа для обёртывания ты использовала хэш SHA-256. Однако при помощи SHA-256 мы вычисляем и «ID токенов». При такой конструкции кто-нибудь сможет развернуть ключи при помощи одного лишь ID токена, который хранится вместе с обёрнутыми ключами; то есть все ключи можно развернуть тривиальным образом. Чтобы исправить это, нам нужно по-другому вычислять хэш при вычислении материала ключа для обёртывания, чтобы по ID токена невозможно было вычислить материал ключа.

Изначально Claude попыталась решить эту проблему, использовав для получения материала ключа PBKDF2 с 100000 итераций.

промпт: PDKDF2 с 100000 итерациями будет очень вычислительно дорогостоящим. Это было бы важно, если входными данными был пароль с низкой энтропией, но необязательно в случае входных данных с высокой энтропией. Давай вместо PBKDF2 применим SHA-256 HMAC со статичным ключом HMAC (который, по сути, будет работать в качестве «соли»).

Claude сгенерировала код, использующий в качестве ключа HMAC строку «OAUTH_PROVIDER_WRAPPING_KEY_HMAC_v1».

промпт: Выглядит довольно неплохо, но ради производительности давай определим WRAPPING_KEY_HMAC_KEY как 32-байтный массив, чтобы его не нужно было кодировать или хэшировать под нужный размер (как это был делал HMAC для ключей большего размера). Вот 32 бита в шестнадцатеричном виде, которые я выбрал случайно, используй их в качестве ключа HMAC: 22 7e 26 86 8d f1 e1 6d 80 70 ea 17 97 5b 47 a6 82 18 fa 87 28 ae de 85 b5 1d 4a d9 96 ca ca 43

(Примечание: здесь использовать жёстко прописанный «ключ» вполне приемлемо: по сути, это HKDF-Extract с фиксированной случайной солью, что для нашего сценария использования подходит. Здесь мы стремимся реализовать следующее свойство безопасности: две стороны должны использовать независимые случайные оракулы, что в данном случае оказывается вполне качественной архитектурой. Возможно, я бы тоже использовал тот же подход для генерации ID токена, но с другой солью; однако это несущественное изменение).

Эта беседа показывает, сколько знаний требуется, когда общаешься с LLM. Созданный Claude посередине сессии «один огромный изъян» остался бы незамеченным кем-то, не обладающим столь же большим опытом в коде криптографии. Кроме того, многие люди бы не подвергли сомнению странное решение перейти на PBKDF2: на самом деле, LLM не «рассуждают» в истинном смысле.

В заключение

Для первой попытки создания библиотеки OAuth результат неплохой, но пока я бы не рекомендовал его использовать. По моему опыту, очень сложно создать корректную и безопасную реализацию провайдера OAuth, и эта задача определённо требует больше времени и внимания, чем было потрачено на эту попытку (пока). На мой взгляд, это неподходящая область для тестирования LLM. В реализации OAuth нашей компании ForgeRock есть сотни багов безопасности, и это несмотря на наличие сотен тысяч автоматизированных тестов, через которые пропускается каждый коммит, моделирование угроз, лучшие SAST/DAST и тщательный аудит безопасности, проводимый специалистами. Нельзя серьёзно воспринимать мысль о том, что LLM способна реализовать за вас такую сложную систему.

История коммитов этого проекта просто потрясает. Очевидно, у инженеров было хорошее понимание многих аспектов дизайна, а LLM внимательно контролировалась и генерировала приличный код. (LLM замечательно справляются с кодингом в таком стиле). Но модель всё равно пыталась принимать дурацкие решения; некоторые из них были отловлены разработчиками, другие нет. И я уверен, что в коде ещё есть проблемы. Была бы ситуация хуже, если бы этим занимался человек? Наверно, нет. Многие подобные ошибки можно найти в популярных ответах на Stack Overflow; вероятно, именно оттуда Claude им и научилась. Но я знаю множество инженеров, которые бы справились лучше, потому что они чрезвычайно старательны. Подобный код требует большого внимания, и здесь очень важны детали. Да, он немного похож на результат «вайб-кодинга», несмотря на заявления в README, но это справедливо и для большой части кода, который писался людьми. LLM или нет, мы не должны плевать на качество кода.

Из своего опыта работы с LLM и из анализа этого проекта я вынес следующее: необходимо чёткое понимание того, какой код вы хотите получить от LLM, чтобы уметь судить, справилась ли она. Чтобы по-настоящему понять, как это выглядит, часто нужно использовать своё мышление в «Системе 2» (чтобы вы не принимали любой результат как наилучший), нужно создать что-то самостоятельно. В случае тривиальных задач, способ реализации которых мне не важен, я с радостью позволю LLM делать всё, что она пожелает. Но в случае важных вещей, например, системы аутентификации, я лучше реализую её сам и тщательно продумаю все аспекты.

Комментарии (6)


  1. Evengard
    09.06.2025 15:39

    Эта беседа показывает, сколько знаний требуется, когда общаешься с LLM. Созданный Claude посередине сессии «один огромный изъян» остался бы незамеченным кем-то, не обладающим столь же большим опытом

    Наверное ключевая фраза всей статьи, которую я и искал и которая подтвердила мои ощущения от этих современных LLM - они могут быть полезны и применимы, но бездумно их использовать (как мечтают очень многие) - не то что нельзя, а просто напросто вредно.

    У меня ощущение что очень мало людей понимают этот сверхважный аспект.


    1. Jijiki
      09.06.2025 15:39

      она может написать плохой код для тех лида или профессора, а новичку может подсветить тему для изучения и показать перспективу не правильным кодом, и сложное для новичка может стать только вопросом времени, когда от простого примера сможет уже сам используя черновик сделать правильно, это метод от обратного)


    1. Zulu0
      09.06.2025 15:39

      Есть хорошая поговорка: "доверяй, но проверяй".


  1. n0isy
    09.06.2025 15:39

    Пожалуй, сохраню эту статью для пруфа, когда хейтеры спрашивают "а где результаты вайбкодинга". Результаты в основном в приватных репах, мои в том числе, а этот можно поглядеть.

    Пожалуй сохраню эту статью для пруфа, когда эффективные менеджеры спрашивают: "оно само работает, ничего проверять не надо". Без эксперта пока не работает. Что вполне ожидаемо и согласуется с моей собственной практикой.


    1. Wesha
      09.06.2025 15:39

      Помнится, Ford Pinto тоже «работал нормально в большинстве случаев»...


    1. zhka
      09.06.2025 15:39

      пока это вопрос эффективности, компании будут искать людей, которые НЕ используют нейронки, ЕСЛИ они сами их используют. Ну и наоборот)