Все статьи
Содержание

    Безопасность веб-приложений: борьба с самим собой, или проводим черту адекватности

    Насколько безопасным должно быть приложение? Для кого-то этот вопрос не имеет смысла. "Настолько, насколько это возможно. Чем безопасней, тем лучше". Но это не исчерпывающий ответ. И он не помогает сформировать security политику в проекте. Более того, если придерживаться только этой директивы ("чем больше security, тем лучше"), мы можем оказать медвежью услугу самим себе. Почему? Ответ узнаете в этой статье.

    Security конфликтует с usability

    Лишние security проверки делают приложение менее удобным. В основном это затрагивает 2 части приложения: аутентификация и восстановление пароля.

    Многофакторная аутентификация, включающая в себя принудительную посылку SMS и ввод дополнительных данных помимо пароля, делает жизнь пользователей чуть более безопасной, но менее счастливой. И если вся суть вашего сервиса заключается в том, что там можно обмениваться картинками с котиками, то пользователь может не оценить ваше рвение сделать его опыт более безопасным.

    Best Security Practices рекомендуют нам в случае ошибок при аутентификации давать пользователю как можно меньше информации о том, где могла произойти ошибка, чтобы злоумышленник не смог собрать базу пользователей. Согласно этим рекомендациям, если посетитель сайта прошел через 33 шага аутентификации и допустил опечатку в одном из полей, лучшим security решением будет написать нейтральное сообщение: "Извините, что-то пошло не так. Попробуйте, пожалуйста, еще раз". Благодарность разработчикам и искреннее восхищение благородными намерениями системы безопасности сайта — это не те эмоции, которые появятся у пользователя в этом случае.

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

    Security делает приложение сложнее в разработке и поддержке

    Чем больше security реализует приложение, тем оно сложнее в разработке. Длительность написание отдельной функциональности может вырасти в несколько раз ради незначительного security улучшения.

    Много мероприятий может тратиться не на фактическую защиту от уязвимостей, а на то, чтобы сделать жизнь киберпреступников более несчастной. Как крайний пример этого случая можно привести обфускацию названия методов REST API и их параметров.

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

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

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

    Есть еще сотни других способов, как сбить злоумышленника с толку. Но, разумеется, они требуют время на реализацию. Эти механизмы могут быть достаточно сложными для понимания и поддержки даже при наличии хорошего кода и сопровождающих комментариев. Но самое главное заключается в том, что эти вещи не закрывают никакие уязвимости, а делаются исключительно для того, чтобы затруднить поиск этих самых уязвимостей.

    Не всегда просто бывает отделить "Грамотно спроектированный и безопасный функционал" от "Безумных игр разума с воображаемым хакером". Тем более, что эта граница плавает в зависимости от того, насколько ваше приложение привлекает потенциальных злоумышленников.

    Security делает приложение сложнее в тестировании

    Все наши правила и проверки должны тестироваться. Unit тесты, интеграционные тесты, ручное тестирование, — нам надо решить, какой подход будет использоваться для каждого конкретного security механизма.

    Просто не тестировать мы не можем. Потому что в код закрадываются ошибки. И если даже мы изначально все написали без ошибок, то в процессе поддержки, модификации, рефакторинга — ошибки обязательно появятся. Никто сразу не пишет legacy код. Он становится legacy кодом постепенно.

    Будет не совсем разумно, если бизнес-функционал подвергается тщательному тестированию, но security механизмы считаются нерушимыми, безупречными и абсолютными.

    Если это все будет тестироваться вручную, то возникает вопрос о том, как часто. Если у нас более-менее сложное веб-приложение, использующее REST API, то в нем потенциально десятки, если не сотни, мест, где может закрасться broken authentication уязвимость. Типичным примером уязвимости является ситуация, когда в HTTP запросе мы меняем какой-то параметр (например, ID пользователя) на другой, и нам возвращается информация, к которой мы не должны иметь доступ. Проверять каждый подобный case — это титанический труд. Надо ли заниматься этим перед каждым мажорным релизом? Нужно ли выделить под это отдельного человека? Или отдельную команду?

    Эти вопросы важны, т.к. broken authentication допустить очень легко. При любом изменении модели, при любом новом REST методе надо всегда думать о том, не появится ли эта уязвимость. Универсального и простого решения нет. Но есть различные подходы, которые позволяют в рамках проекта бороться с проблемой консистентно. Например, в нашей платформе CUBA мы используем роли и группы доступа, которые позволяют настраивать, какие сущности кому доступны. Все еще остается много работы по настройке правил, но эти правила единообразны и консистентны, что облегчает поддержку.

    Помимо broken authentication есть еще десятки security проблем, которые тоже желательно проверять. И, внедряя новые проверки и механизмы, мы должны подумать, как это будет тестироваться. Вещи, которые не тестируются, со временем имеют свойство ломаться. И тогда у нас мало того, что не будет нормального security, так еще и будет присутствовать ложная уверенность в том, что он есть.

    Особые проблемы доставляют 2 категории защитных механизмов: те, которые включены только для продакшена, и те, которые представляют из себя защиту второго (третьего, четвертого) уровня.

    Защита, которая реализована только на продакшене. Допустим, session token cookie должен иметь галочку https. Но, если на среде тестирования используется http, это значит, что существуют отдельные конфигурации для тестирования и продакшена. Следовательно, тестируется не совсем то, чем будут пользоваться. И, если при миграции или каких-то изменениях эта галочка перестанет проставляться, мы можем этого сразу не заметить. Как решать эту проблему? Вводить еще один environment для пред-продакшена? Если да, то какую часть функциональности тестировать на нем?

    Многоуровневая защита. Люди, искушенные в безопасности, любят создавать защиту, которую можно проверить, только если сломался первый барьер. Идея разумная. Если даже злоумышленник нашел уязвимость в первом эшелоне, он застрянет на 2-м. Но как это тестировать? Типичный пример — это использование различных db пользователей для различных пользователей приложения. Даже если наш REST содержит broken authentication, то злоумышленник не сможет отредактировать/удалить данные, т.к. у его db пользователя не будет прав. Разумеется, эти конфигурации устаревают, ломаются, если их не поддерживать и не тестировать.

    Много security может сделать приложение менее секьюрным

    Чем больше security проверок, тем больше сложность. Чем больше сложность, тем больше вероятность допустить ошибку. Чем больше вероятность допустить ошибку, тем security хуже.

    Возьмем для примера снова форму логина. Логин с двумя полями (username и password) реализовать просто. Надо всего лишь проверить, есть ли такой пользователь в системе и правильно ли введен пароль. Ну, еще надо обязательно проверить, что логин/пароль идут в теле запроса, а не в URL параметрах. Желательно убедиться, что приложение не подсказывает, где ошибка в логине или пароле — чтобы нельзя было собрать базу со списком пользователей (этот пункт можно игнорировать для некоторых приложений во имя лучшего user experience). И обязательно надо убедиться, что реализован anti-bruteforce механизм. Который, разумеется, должен быть устойчив к fail-open уязвимости. Еще лучше, если мы не будем показывать взломщику, что мы пометили его как "взломщик" и начнем просто игнорировать его реквесты — пусть и дальше думает, что взламывает логин. Да, и надо не забыть убедиться, что мы нигде в логах не записываем переданный пароль. Ну, и еще несколько тонкостей, о которых надо помнить. В общем, что может быть проще обычной формы логина с двумя полями?

    Другое дело, мультифакторная аутентификация. Где нам посылают какой-то токен на почту или по SMS. Или где нужно ввести какие-то дополнительные данные. Или надо пройти по целому ряду шагов, постепенно вводя информацию о себе. Все это сложно. По идее, все эти дополнительные проверки должны понизить вероятность взлома. Если все реализовано правильно, то это действительно так. Вероятность взлома остается (ни SMS, ни e-mail сообщения, ни что-либо другое не дает 100% гарантии от взлома), но понижается. Но и без того достаточно сложная логика аутентификации становится еще более сложной, и где-то допустить ошибку становится проще. Наличие хотя бы одной ошибки или одного неверного допущения делает всю эту модель менее секьюрной, чем простая форма с 2-мя полями.

    Более того, назойливые и неудобные меры безопасности могут вынудить пользователей хранить свою информацию менее секьюрно. Например, если в корпоративной сети нужно менять пароль каждый месяц, то пользователи, которым абсолютно непонятны эти странные требования, могут просто начать писать свой пароль на стикере и клеить его к монитору. "Это проблема пользователей, если они допустят такую глупую оплошность" — можете возразить вы. Но нет. Это наша с вами проблема. В конце концов, разве не для этих самых пользователей мы и делаем наши приложения?

    Замечательно. Что ты предлагаешь?

    Я предлагаю с самого начала разработки определиться, насколько далеко мы готовы идти, чтобы обмануть злоумышленника. Готовы ли мы оптимизировать нашу логин форму так, что время ответа на запрос логина никогда не выдаст, существует ли пользователь с таким именем или нет? Готовы ли реализовывать такие проверки при аутентификации, что даже друг жертвы взлома с ее телефоном в руках и с ее компьютера не способен зайти в наше приложение? Готовы ли мы усложнять разработку в разы, увеличивать значительно бюджет и длительность разработки, жертвовать хорошим user experience ради того, чтобы сделать жизнь злоумышленника чуть сложнее?

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

    В худшем случае, проект может потратить много ресурсов на возведение непроницаемой стены против одного типа атаки, когда в приложении в другом месте присутствует огромная security брешь.

    Делая выбор, будем ли мы работать над тем или иным security механизмом, или будем ли мы настраивать еще один уровень защиты, мы должны учитывать множество факторов:

    • Насколько легко уязвимость эксплуатировать? Broken authentication эксплуатируется очень легко и не требует для этого технического бэкграунда. Следовательно, мы должны уделить этой проблеме значительное внимание.
    • Насколько критической является уязвимость? Если злоумышленник получает доступ к информации других пользователей или может изменить информацию других пользователей, то, очевидно, это очень важная проблема. Если злоумышленник может собрать ID определенных продуктов в нашей системе, но не имеет возможности ничего сделать с этими ID — то важность проблемы в разы ниже.
    • Насколько повысится безопасность сайта, если мы это реализуем? Если речь идет не о первом уровне защиты (например, проверка XSS проблем на инпуте, когда уже реализован хороший механизм для санитизации аутпута) или речь идет просто о том, что запутать злоумышленника больше (вместо блокировки его действий мы начинаем игнорировать его запросы, "притворяясь", что мы не распознали атаку), — приоритет этих изменений не очень высокий, если они вообще должны быть реализованы.
    • Сколько это займет времени / будет стоить?
    • Насколько ухудшится опыт пользователей в результате этого изменения?
    • Насколько сложно это будет поддерживать и тестировать? Частая практика не использовать код 403 при попытке обратиться к запрещенному ресурсу, а всегда возвращать 404, чтобы затруднить сбор информации о существующих идентификаторах. Это решение, хотя и имеет смысл и действительно делает жизнь злоумышленника сложней, в то же самое время может усложнять тестирование, анализ ошибок в продакшене и даже затрагивать положительных опыт пользователей, когда вместо того, чтобы сообщить им, что у них недостаточно прав для получения этого ресурса, мы говорим, что такого ресурса нет в принципе.

    Да. В вашем конкретном случае сложная мультифакторная аутентификация может быть и нужна. Но вы должны отдавать себе отчет в том, насколько это затрудняет поддержку и разработку и насколько это делает ваше приложение менее приятным для пользователей.

    Ты оправдываешь пренебрежительное отношение к безопасности

    Никоим образом. Есть security-чувствительные приложения, где дополнительная защита может быть нужна даже в ущерб опыту пользователей и стоимости разработки.

    Ну, и в любом случае, есть ряд уязвимостей, которые непозволительно иметь, независимо от того, для чего предназначено приложение. CSRF — пример такой уязвимости. Борьба с ней однозначно не делает опыт пользователей хуже и не сильно затрудняет разработку. Многие server-side (например, Spring MVC) и client-side (например, Angular) фреймворки позволяют легко поддержать CSRF-токены из коробки. Далее, с помощью того же Spring MVC добавить несколько необходимых хеадеров (Access-Control-* хеадеры, Content-Security-Policy) — это тоже достаточно простая задача.

    Broken authentication, XSS, SQL injections и некоторые другие уязвимости нельзя иметь у себя в приложениях. Защита от них проста для понимания и разобрана по полочкам в многочисленных книгах и статьях. Передача sensitive информации в URL параметрах или хранение слабо захешированных паролей относятся сюда же.

    В идеале, в проекте должен быть манифест, который регулирует security политику: что и как защищаем, описание password policy, что тестируется и т.д. Этот манифест будет различным для разных проектов. Проекты, где присутствует вставка ввода пользователя в команду операционной системы, будут содержать описание защиты от injection OS commands. Проекты, где пользователь может загружать свои файлы на сервер (включая аватар), требуют описание базовых уязвимостей, которые могут появляться в этом случае.

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

    Если команда является небольшой и технический руководитель неизменен и имеет выработанную политику по поводу security, то постоянное code review может быть достаточным, чтобы избежать подобных проблем даже без наличия манифеста.

    Выводы:

    • При работе над security нужно учитывать то, насколько чувствительным к безопасности является сайт. Приложения для банков и приложение для опубликования анекдотов требуют разных подходов.
    • При работе над security нужно учитывать, насколько ухудшится user experience в результате того или иного изменения.
    • При работе над security нужно учитывать, насколько это решение сделает код сложней и поддержку болезненней.
    • Security механизмы надо тестировать.
    • В команде желательно ввести просветительную деятельность по проблемам security и/или подвергать всю работу тщательному security review.
    • Есть уязвимости, от которых желательно избавиться для веб-приложений любого уровня: xss, xsrf, injections (включая SQL), broken authentication, etc.
    Jmix - это open-source платфора быстрой разработки бизнес-приложений на Java