PXLSecurity LTD., София, България Офанзивна сигурност от 2014[email protected]
Сигурност на уеб приложенияМетодологияЗащита

Защитни механизми, които присъстват, но не работят

От PXLSec1 октомври 2026 г.17 мин. четенеОбновено:

Конфигуриран контрол може да не налага замисленото свойство. Хедър може да бъде пренебрегнат заради друг механизъм с предимство, allowlist да приема нежелани заявители или валидатор да допуска при грешка. Важното е наблюдаваната ефективност, а не наличието на конфигурация.

Контролът, който покрива контролния списък, но не спира атаката

Липсващ контрол е проста констатация. Някой го е забравил, скенер забелязва, че отсъства, отстраняването е да бъде добавен. Всеки разбира проблема в рамките на изречение.

Инертен контрол е различен случай. Конфигурацията е налице. Хедърът се излъчва при всяка заявка. Низът на политиката е дълъг, конкретен и явно продукт на умишлена работа. Той се появява в базовите изисквания за укрепване на защитата и в реда за съответствие, който казва внедрено. А при заявка, в истински браузър, той няма ефект върху изхода, който е избран да предотврати.

Само проверка за наличие пропуска взаимодействия между хедъри, браузърно поведение и състояние. Поведенчески инструменти и човешки преглед могат да ги оценяват, когато са насочени към реалния отговор и очакваната политика. Тествайте комбинацията както се предоставя, включително промени от проксита и CDN.

Тя е също така систематично недодокладвана, по причини, които нямат нищо общо с компетентността:

  • Инструментариумът оценява наличие. Оценител на хедъри, който намери Content-Security-Policy и X-Frame-Options в един отговор, докладва два контрола, не конфликт.
  • Доказателството е самопотвърждаващо се. Артефактът, доказващ, че контролът съществува, съдържа също и причината той да не работи, а никой не чете втората му половина.
  • Никой не отговаря за взаимодействието. Хедърите се натрупват от стойност по подразбиране на рамка, прокси, правило на CDN, базова линия за сигурност и междинен слой на приложението. Всеки собственик валидира собствения си принос; никой не отговаря за крайния резултат.
  • Повторното тестване търси регресии, не неефективност. Контрол, който никога не е работил, не е промяна, а промяната е това, което проверката открива.
  • Оценката е ниска, затова е евтино да бъде отложена. Те попадат като информационни или ниски, приемат се и седят с години, като в документацията за съответствие все още пише внедрено.

Последствието е позиция, надценена точно на местата, където е била най-съзнателно адресирана. Екипът без политика за clickjacking знае, че няма такава. Екипът с две противоречащи си вярва, че е защитен.

Когато два HTTP хедъра си противоречат, браузърът избира един

Най-чистият пример е защита от clickjacking, обявена два пъти, в два механизма, с противоположни значения. Отговор носи и Content-Security-Policy с широко отворена директива frame-ancestors, и ограничителен остарял хедър:

Content-Security-Policy: default-src 'self'; script-src 'self'; frame-ancestors *
X-Frame-Options: SAMEORIGIN

Прочетен изолирано, вторият ред е правилен и конвенционален контрол: този документ може да бъде рамкиран само от страници на собствения му origin. Прочетен като двойка, той е украса.

Правилото за предимство е изрично в спецификацията Content Security Policy и реализирано последователно в текущите браузърни двигатели: където налагана CSP, съдържаща frame-ancestors, е налице, потребителският агент зачита тази директива и игнорира X-Frame-Options изцяло. CSP беше проектирана да измести остарелия хедър, а изместването е пълно, а не сливане — без сечение, без „най-ограничителното печели“, без резервен вариант. Директивата на CSP е политиката, а frame-ancestors * позволява рамкиране от всеки origin.

И така, отговорът рекламира два контрола и налага разрешителния. Оценител на хедъри присъжда точки за двата. Оператор, четящ конфигурацията, вижда SAMEORIGIN и заключава, че приложението не може да бъде рамкирано. То може да бъде рамкирано от всеки.

Намерихме това независимо в отделни ангажименти, без връзка между кодовите бази или екипите — което го прави достойно да бъде написано, а не заведено като еднократен случай. Нито едно не беше маркетингов сайт. И двете бяха удостоверени интерфейси на контролен слой и операции, където прикрито кликване струва нещо на нападател. Точно където рамкирането има най-голямо значение, защитата беше обявена два пъти и наложена разрешително.

Механизмът е банален: някой, несигурен кои origin-и легитимно вграждат приложението, пише *, съществуващият X-Frame-Options остава, защото премахването на хедър за сигурност се усеща като регресия, а никой не свързва двата реда.

Как да укрепите защитата

  • Третирайте frame-ancestors като единствения източник на истина и го задайте на истинския отговор — 'none', ако приложението никога не е легитимно вграждано, или изричен списък с origin-и, ако е. Никога *.
  • Пазете X-Frame-Options само като съвместимост с наследени системи и се уверете, че той е съгласен с директивата на CSP. Несъгласието е дефект само по себе си, независимо кой от двата печели.
  • Проверявайте комбинацията, не наличието: парсвайте и двата хедъра в CI и спирайте компилацията, когато frame-ancestors отсъства, е със заместващ знак или е несъвместим с X-Frame-Options. Тествайте отговора както се обслужва, тъй като прокси, CDN или шлюз може да го пренапише след приложението.
  • Избройте легитимните вграждащи умишлено. Заместващ знак почти винаги е заместител на „не искахме да разберем“, а отговорът обикновено е „нищо не рамкира това“.

Вграждането в рамка носи по-голям риск за работен екран, отколкото за информационна страница

Clickjacking рутинно се оценява като нисък, и за голям клас страници това е правилно: на страница със съдържание без удостоверено състояние и без действия, най-лошият изход е объркващо потребителско преживяване. Тестери, които срещат проблема главно на брошурен софтуер, усвояват ниската оценка като свойство на класа.

То не е свойство на класа, а на страницата. Clickjacking няма собствено въздействие; то заема въздействието на каквото и да е действие, което жертвата може да бъде подведена да предприеме, докато е удостоверена. Въпросът никога не е „колко сериозен е clickjacking“, а „кое е най-вредното единично взаимодействие на този екран за влязъл потребител, и колко струва да бъде задействано безшумно?“.

На операционен интерфейс отговорът става неудобен бързо, защото рамкираното действие движи пари, променя системно състояние или подава лични данни от името на жертвата — с прикачени нейната сесия, права и одитна идентичност. Два абстрахирани примера от собствената ни работа:

  • Форма за приемане на лични данни. Рамкирана под контролирано от нападателя съдържание, тя става канал за подаване на записи под сесията на легитимен оператор, или — с внимателно наслагване и прозрачност — за прихващане на това, което потребителят въвежда в това, което смята за страницата на нападателя. Това е събитие за защита на данните, не козметично.
  • Екрани на платежен двигател, които променят състояние. Където едно кликване за потвърждение освобождава, спира, пренасочва или одобрява транзакция, рамкирането е примитив за дистанционна промяна на състояние с правдоподобна одитна следа, сочеща към погрешния човек. Дефектът гласи „липсваща защита от рамкиране“; изходът е неоторизирано финансово действие, приписано на доверен оператор.

И двете бяха защитени на хартия. Политиката за рамкиране беше обявена. Тя просто беше от инертния вид.

Бележка. Критичността трябва да следва правата и действието, достижими на конкретния екран, не класа уязвимост в абстрактното. Същият липсващ контрол заслужава ниска оценка на публична страница и съществено по-висока там, където удостоверен екран извършва необратима операция с едно кликване.

Отразяване на произволен източник в CORS

Еквивалентът на ниво origin е cross-origin политика, която отговаря на всеки въпрос с да: сървърът отразява Origin на заявката право обратно като Access-Control-Allow-Origin.

GET /api/account/profile HTTP/1.1
Origin: https://attacker.example.test

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://attacker.example.test
Content-Type: application/json

Това е, отново, контрол, който съществува. Има CORS конфигурация, тя не е обобщаваща *, затова се чете като обмислен списък за допускане, и удовлетворява всяка точка от проверочен списък, питаща дали cross-origin достъпът е ограничен. На практика тя съвпада с всеки origin, който пита — списък за допускане с допълнителни стъпки, и в едно отношение по-лошо от *, тъй като заместващ знак не може да бъде комбиниран с данни за достъп, докато отразен origin може.

Наблюдавано е отразяване на произволен origin без Access-Control-Allow-Credentials. Това само по себе си не доказва кражба на удостоверени отговори. Без необходимия CORS хедър JavaScript не може да прочете credentialed cross-origin отговор. Дали заявката се изпраща с cookies зависи от режима, политиката за cookies и, когато е приложимо, preflight. Блокираното четене не доказва, че не е извършено удостоверено действие. Оценете чувствителността на отговора и поведението на браузъра; бъдеща промяна в поддръжката на credentials изисква нов преглед.

  • Данните за достъп се включват. На разработчик му трябва извикване с удостоверяване чрез бисквитка да работи от клиентски интерфейс от друг origin, затова добавя Access-Control-Allow-Credentials: true, а информационна констатация става „прочети-всичко-като-жертвата“. Промяната изглежда локална и разумна, а никой не преоценява политиката за origin.
  • Токен се премества в четим отговор. Bearer токени, CSRF токени, идентификатори на сесии или подписани URL адреси в тяло, което всеки origin може да прочете, превръщат неудостоверена крайна точка в начална стъпка за удостоверен достъп.
  • Мрежовата позиция може да има значение. Браузърът може да достига вътрешна или VPN услуга, недостъпна директно за нападателя. Експлоатируемостта зависи и от браузърните защити за частни мрежи, транспортните ограничения, приетата заявка и чувствителността на отговора; самото отразяване на origin не доказва работещ път.

Разрешение за http://localhost:3000 в production дава достъп на документи от точно тази схема, име и порт. Затова локално приложение, което контролира този origin, може да има значение. Друго име, сочещо към същия loopback адрес, е различен origin; DNS резолюцията сама не го прави съвпадащо. Проверете дали development origin е необходим и какви данни може да чете.

Първопричината е една и съща и в двата: списък за допускане беше написан, затова контролът е налице и документиран, а никой не преосмисли какво позволява той.

Как да укрепите защитата

  • Проверявайте входящия Origin спрямо изрична политика за средата, преди да го върнете в Access-Control-Allow-Origin. Не отразявайте произволни origins. Точното съвпадение е прост безопасен вариант; по-широките правила изискват целенасочен преглед.
  • Третирайте Access-Control-Allow-Credentials: true като промяна, която налага политиката за origin да бъде преразгледана. CORS с данни за достъп и разрешителен списък за допускане не бива никога да се срещат.
  • Дръжте origin-ите за разработка извън продукционната среда по конструкция — конфигурация за всяка среда, с проверка при внедряване, която се проваля при loopback, записи от частен диапазон и със заместващ знак.
  • Изпращайте Vary: Origin навсякъде, където отговорът зависи от origin-а, така че кешовете да не могат да обслужат политиката на един извикващ на друг.
  • Избирайте разрешените origins според чувствителността на отговора и очакваните заявители. Преглеждайте политиката при промяна в credentials или чувствителните полета; не приемайте бъдещо излагане за неизбежно или публични cross-origin данни за уязвимост по принцип.
  • Тествайте противниково, не функционално: изпратете несвързан origin, null origin, поддомейн и вариации с представка или наставка на легитимен origin, и потвърдете, че всеки е отказан, а не съвпаднат.

Проверката, която приема всичко

В едно приложение валидаторът на анти-CSRF хедъра четеше хедъра, сравняваше го с очакваната стойност на сесията и връщаше успех по всеки път извън сравнението. Няма хедър: прието. Хедър, зададен на invalid: прието. Хедър, копиран от сесията на друг потребител: прието. Функцията беше скъп начин да се напише return true — мъртъв код, който се чете като контрол при преглед и се появява в модела на заплахите като „CSRF защита: валидиране на хедър“. Валидаторите достигат това състояние чрез натрупване, не чрез един небрежен коммит: написан строг, разхлабен за наследен клиент, рефакториран в помощна функция, която логва, вместо да хвърля.

По-важното е как оцелява тестването. Повечето хора проверяват анти-CSRF контрол, като го премахват: махат хедъра, възпроизвеждат, очакват отказ. Но пропускането и несъответствието са различни пътища на кода, а валидатор, разхлабен за съвместимост, държи клона за пропускане непокътнат — това е именно грешката, която разработчиците виждат, когато забравят хедъра. Заобикалянето живее в сравнението, затова тествайте сравнението.

POST /api/v1/account/email HTTP/1.1
Host: app.example.test
Cookie: session=<valid session>
X-Csrf-Token: aaaaaaaaaaaaaaaaaaaaaaaa
Content-Type: application/json

{"email":"attacker@example.test"}

Тествайте липсваща, празна, грешна стойност и стойност от друга сесия. Приетото несъответствие показва неефективна валидация по тествания път, но не непременно липса на всички CSRF защити. Преди твърдение за експлоатируем браузърен CSRF проверете реалистична cross-site заявка, cookies, Origin проверки, preflight и защитеното действие в договорения обхват.

Как да укрепите защитата

  • Отказът трябва да е поведението по подразбиране: изчислете очакваната стойност и я сравнете с получената. Ако изчисляването или сравняването е неуспешно, върнете грешка и спрете обработката. Отхвърляйте липсващите, празните и неправилно оформените стойности на едно място, за да няма различни пътища за заобикаляне.
  • Проверявайте отказа при несъответствие в тестовете, не само при пропускане; набор, който никога не изпраща погрешен токен, не може да забележи, че сравнението е спряло.
  • Налагайте в междинен слой, покриващ всички методи, променящи състояние, по подразбиране, със списък за освобождаване, преминал преглед, не извиквания за всеки обработчик, които могат да отпаднат.

Токен, който пътува в URL

Второ приложение валидираше анти-CSRF токена си правилно, с истинско сравнение, но го приемаше като параметър в query string на заявки, които променяха състояние през GET:

GET /admin/users/delete?id=4192&csrf=6f1c9d2ab74e HTTP/1.1
Host: app.example.test
Cookie: session=<valid session>

Две грешки се натрупват и имат различни поправки. Промяната на състояние през GET прави операцията достижима отвсякъде, където може да бъде поставен URL — таг за изображение, имейл, предварително извличане, браузър, възстановяващ раздели — и я кара да изглежда идемпотентна за инфраструктура, която не очаква повторен опит да изтрие потребител.

Токен в query string може да попадне в access logs, системи за наблюдение, браузърна история, копирани връзки и отметки. Изтичането чрез Referer зависи от ефективната политика и получателя: strict-origin-when-cross-origin премахва пътя и query при cross-origin заявки, но same-origin заявките могат да ги запазят. Не поставяйте CSRF токени в URL и ограничете записването и споделянето на URL адреси, които дават достъп.

Поправете и двете половини: промени на състоянието през POST, PUT, PATCH или DELETE, токен в хедър или тяло, а параметърът в query string — отхвърлен направо. Резервен вариант, който се приема, е поведението.

Бележка. Същото разсъждение покрива bearer токени, връзки за нулиране на парола, токени за покана и подписани URL адреси за изтегляне. Ако дава нещо и е в URL, приемете, че е в журнал, който не контролирате.

В три отделни приложения намерихме същата предудостоверителна бисквитка, описана в кода като подпис и използвана в потока за вход. Тя се излъчваше в обслужвания HTML, задаваше се преди удостоверяване и беше байт за байт идентична за всеки анонимен посетител: константа, пременена като стойност за всеки посетител.

Работата на такава бисквитка е обвързване — свързване на браузъра, който е започнал OIDC поток, с онзи, който се връща към callback-а, така че callback, доставен на погрешния браузър, да бъде отхвърлен. Константа не може да направи това: ако стойността е една и съща за вас и за нападателя, притежаването не доказва нищо и проверката винаги минава. Доказателството изглеждаше отлично — зададена бисквитка, проверен подпис — а приносът беше нула.

Обвързвайте всяка login транзакция с браузъра, който я започва, чрез подходяща непредвидима стойност и правила за изтичане и повторно използване. Сървърно състояние или подходящо защитени cookies могат да осигурят връзката; атрибутите на cookies трябва да съответстват на callback процеса. Постоянна обща cookie не различава транзакциите, но оценката на целия процес изисква проверка и на останалите механизми.

Вход, изграден без частите, които го правят безопасен

Потокът, който тази бисквитка трябваше да защитава, се оказа OIDC вход с код за оторизация без state, без PKCE и без nonce. И трите пропуснати. Потребителите влизаха успешно, а интеграцията беше преминала прегледа на дизайна с диаграма на последователността, която беше, както беше начертана, правилна.

    state може да обвързва callback с браузъра, започнал процеса, и да пренася защитено приложно състояние. Липсата му сама не доказва login CSRF, ако друг подходящ механизъм, например правилно наложен PKCE, осигурява защитата. PKCE обвързва authorization кода с verifier, пазен от клиента, който започва процеса. При S256 authorization заявката съдържа challenge, а не verifier; verifier се изпраща по-късно към token endpoint през TLS. Проверете поддръжката, обвързването с транзакцията и защитата срещу downgrade, преди да разчитате на PKCE за CSRF защита.
  • nonce защитава срещу повторение и инжекция на ID токени: пренесен в токена и проверен при връщане, той позволява на разчитащата страна да твърди, че токенът е бил издаден за тази заявка, а не взет от друг контекст.

Дефектът продължава, защото е невидим на височината, където се случват повечето прегледи. Диаграма показва потребителя, разчитащата страна, доставчика и стрелките между тях, а всяка от тези стрелки съществуваше и беше правилна. state, PKCE и nonce са параметри на две от тях, под разделителната способност на диаграмата. Никога не ги виждате липсващи в архитектурен документ; виждате го незабавно в прокси журнал.

Как да укрепите защитата

  • Прихванете заявката за оторизация и callback-а за всяка интеграция за вход и прочетете параметрите; диаграма или име на библиотека не е доказателство.
  • Използвайте PKCE със S256 и обвържете login транзакцията с браузъра, който я започва. Прилагайте state и nonce според избрания flow и изискванията и проверявайте използваните стойности. Не класифицирайте целия процес само по наличието или липсата на един параметър.
  • Съхранявайте стойностите, генерирани преди началото на потока от страна на сървъра, свързани с предудостоверителната сесия, или в бисквитка с HttpOnly, Secure и SameSite, всяка с кратък срок на валидност и еднократна употреба.
  • Проверявайте издателя, получателя, подписа, срока и останалите приложими claims на ID токена. Ако е изпратен nonce, проверете съвпадението му преди употреба. Декодирането на claims не е проверка на подписа или стойностите.

Ограничения за достъп, реализирани в браузъра

Административен интерфейс скриваше привилегировани екрани от потребители без относимата роля: навигационните записи потиснати, маршрутите отказващи да се изобразят, бутоните не се показваха. Всяко от тези решения беше взето в JavaScript, след като сървърът беше изпратил страницата, от твърдение за роля, което клиентът държеше. API-то зад него удостоверяваше извикващия и извършваше записа. Портата беше истинска за всеки, използващ приложението както е доставено, отсъстваща за всеки, изпращащ заявки пряко — а клиентският интерфейс вече беше документирал всяка привилегирована крайна точка.

Вариант се появи другаде, и той е по-поучителният, защото инженерството беше наистина добро: щателно откриване на подправяне, множество независими сигнали, няколко неудобни за побеждаване поотделно, покриващи различни категории намеса. После те бяха сведени до една булева стойност в състоянието от страната на клиента, което всяка защита надолу по веригата консултираше. Променете тази стойност или подменете функцията, която я изчислява, и цялото откриване докладва чисто.

Първопричината и в двата случая не е грешка в кодирането. Тя е реализирането на контрол там, където нападателят командва. Браузърът, мобилният клиент и настолното приложение са територия, която нападателят притежава изцяло: те могат да променят кода, да променят данните, върху които той работи, и да лъжат за резултата. Проверка, изпълняваща се там, може да повиши цената на случайната злоупотреба, но не може да налага, защото налагането изисква налагащата страна да реши. Полезният въпрос не е „правилна ли е тази проверка?“, а „кой решава отговора?“.

Как да укрепите защитата

  • Оторизирайте на сървъра, за всяка заявка, в точката на записа, използвайки идентичност, изведена от сесията, а не каквато и да е стойност за роля, която клиентът е предоставил.
  • Третирайте скриването от страната на клиента само като представяне и приемете, че всяка крайна точка, която клиентският интерфейс може да достигне, ще бъде извикана пряко от някого, който не бива да я достига.
  • Избройте повърхността на API независимо от потребителския интерфейс и упражнете всяка привилегирована операция с нископривилегирован и с неудостоверен токен.
  • Докладвайте сигналите от страната на клиента на сървъра като отделни, приписими наблюдения и решавайте от страна на сървъра, вместо да ги сливате в една клиентска присъда.

Как да проверите за неработещи контроли

Нищо от това не се намира чрез сканиране или четене на документация. Намира се чрез метод, който е досаден, а не труден.

Назовете какво контролът трябва да предотврати, после опитайте точно това. Не нещо съседно, и не тест за наличието му. „Този токен предотвратява cross-site заявка да промени имейл адреса на потребителя“ е проверимо твърдение: изработете тази заявка и я изпратете. Контролите стават инертни в пропастта между името им и ефекта им.

Изпращайте погрешни стойности, не отсъстващи. Пропускането тества проверка за наличие; несъответствието тества логиката за валидиране. За всеки токен, подпис, хедър и подписан параметър: подайте изглеждаща валидна, но неправилна стойност, една от друг потребител или сесия, една от изтекъл контекст и една вече използвана. Отсъствието обикновено се проваля безопасно по случайност; погрешността е там, където контролът или работи, или не.

Тествайте API-то пряко, а не през интерфейса. Интерфейсът е най-добрата защита на контрола и най-малко вероятната входна точка на нападателя. Движете API-то с клиент, който не споделя допусканията на клиентския интерфейс: без скрити полета, без последователност, без филтриране по роля.

Сравнете обявената политика с наблюдаваното поведение. Третирайте хедъри, политики, конфигурация и матрици на роли като твърдения, после измерете работещата система спрямо всяко. Обявеното и доставеното се разминават постоянно: дефинирано, но не изпратено, изпратено в режим само за отчет, наложено на един хост, а не на друг. Обявлението е хипотеза; отговорът е доказателство.

Разграничавайте доказана ефективност, доказан пропуск и непроверено покритие в регистъра на контролите. Когато няма доказателство за съответното поведение, запишете пропуска и възложете проверка, вместо да приемате несигурността за доказана липса на контрол.

Прилагат ли защитните ви контроли реални ограничения?

Тестваме това, което контролът трябва да предотврати, като опитваме точно това — така изплуват мъртвите контроли.

Заявете предложение