Ста­тичес­кий ана­лиз безопас­ности при­ложе­ний (SAST) нужен для ана­лиза исходно­го кода или байт‑кода без выпол­нения прог­раммы. Эта допол­нитель­ная про­вер­ка помога­ет заранее обна­ружить уяз­вимый код. Одна­ко с нас­трой­ками по умол­чанию SAST мало­эффекти­вен. В этой статье пос­мотрим, как мож­но его улуч­шить.

В пре­дыду­щей статье мы показа­ли, как находить уяз­вимос­ти в при­ложе­ниях с помощью динами­чес­кого ска­ниро­вания (DAST) на при­мере ска­нера Nuclei. Одна­ко DAST — это лишь один из под­ходов к пос­тро­ению безопас­ности. В этот раз рас­смот­рим ста­тичес­кий ана­лиз и прин­ципы, на которых он стро­ится.

Ты, воз­можно, стал­кивал­ся со ста­тичес­кими лин­терами в популяр­ных IDE вро­де VS Code. Они ана­лизи­руют исходный код и дают рекомен­дации по улуч­шению. При­мер­но так же работа­ет и SAST, толь­ко с укло­ном в безопас­ность.

Бла­года­ря SAST спе­циалис­ты, ответс­твен­ные за безопас­ность кода, backend-раз­работ­чики, инже­неры Application Security, сот­рудни­ки DevSecOps и DevOps могут нем­ного рас­сла­бить­ся, так как есть допол­нитель­ный уро­вень про­вер­ки и проб­лемный код не сра­зу попадет в про­дук­товое окру­жение.

Од­нако с нас­трой­ками по умол­чанию SAST не отли­чает­ся высокой эффектив­ностью. Что­бы выжать из него мак­симум, нуж­но добавить свои нас­трой­ки — в иде­але для кон­крет­ного при­ложе­ния. Мы покажем, как это делать.

В качес­тве инс­тру­мен­та я выб­рал Semgrep, как самый прос­той спо­соб поз­накомить­ся со ста­тичес­ким ана­лизом.

На­пишем три пра­вила для обна­руже­ния уяз­вимос­тей в API:

  1. Broken Object Level Authorization.
  2. Broken Function Level Authorization.
  3. SQL Injection.

В качес­тве уяз­вимой кодовой базы сно­ва возь­мем спе­циаль­ное при­ложе­ние для тес­тирова­ния защищен­ности API — VAmPI.

За­пус­кать нам его в этот раз не нуж­но, дос­таточ­но прос­то ска­чать исходный код:

git clone https://github.com/erev0s/VAmPI
cd VAmPI

Для ска­ниро­вания при­ложе­ния локаль­но уста­новим Semgrep. Он дос­тупен на всех популяр­ных опе­раци­онных сис­темах, а инс­трук­ция по уста­нов­ке есть на офи­циаль­ном сай­те. Уни­вер­саль­ный спо­соб — ста­вить через пакет­ный менед­жер pip:

python3 -m pip install semgrep

Что­бы запус­тить Semgrep со встро­енны­ми пра­вила­ми, нуж­но перей­ти в дирек­торию с исходным кодом тес­тиру­емо­го при­ложе­ния и выпол­нить сле­дующую коман­ду:

semgrep --config=auto

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

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

Од­нако мы решим проб­лему ина­че: добавив собс­твен­ные пра­вила. Но сна­чала рас­смот­рим, как устро­ен SAST.

Статический анализ безопасности

Что­бы понять, как работа­ет SAST, рас­смот­рим четыре под­хода:

Как работает SAST?

Те­перь по шагам пос­мотрим, что, собс­твен­но, дела­ет SAST.

  1. Ана­лизи­рует исходный код. SAST-инс­тру­мен­ты ана­лизи­руют исходный код, исполь­зуя AST, CFG, DFG и дру­гие модели, что­бы получить пол­ное пред­став­ление о струк­туре и логике прог­раммы.
  2. Ищет пат­терны уяз­вимос­тей. При­меняя раз­личные пра­вила и пат­терны (нап­ример, исполь­зование небезо­пас­ных фун­кций, неп­равиль­ная обра­бот­ка оши­бок), инс­тру­мен­ты SAST обна­ружи­вают потен­циаль­ные уяз­вимос­ти.
  3. Соз­дает отче­ты. SAST-инс­тру­мен­ты генери­руют отче­ты с опи­сани­ем най­ден­ных уяз­вимос­тей, ука­зывая на проб­лемные мес­та в коде.

Semgrep — это как grep?

Ду­маю, ты зна­ком с grep — это популяр­ный инс­тру­мент для поис­ка тек­сто­вых строк с помощью регуляр­ных выраже­ний. О струк­туре кода, впро­чем, он ничего не зна­ет. Пред­ставь grep, который понима­ет код так же, как и ты. Semgrep не прос­то ищет сов­падения по тек­сту, а учи­тыва­ет син­таксис и струк­туру кода.

Пишем правило для Semgrep

Пра­вила в Semgrep опи­сыва­ются на YAML.

rules:
- id: no-eval
pattern: eval(...)
message: "Avoid using eval, as it can lead to security vulnerabilities."
languages: [python, javascript]
severity: warning

Каж­дое пра­вило обя­затель­но содер­жит:

В при­мере выше Semgrep будет искать исполь­зование фун­кции eval в коде на Python и JavaScript и выдавать пре­дуп­режде­ние, так как исполь­зование eval может быть небезо­пас­ным.

Опе­ратор «мно­гото­чие» — пат­терн для нуля или более эле­мен­тов, таких как аргу­мен­ты, опе­рато­ры, парамет­ры, поля, сим­волы.

Taint analysis

Taint analysis в Semgrep — это метод отсле­жива­ния дан­ных, которые пос­тупа­ют из ненадеж­ных источни­ков (нап­ример, вво­да поль­зовате­ля) и могут быть исполь­зованы в опас­ных кон­тек­стах (нап­ример, в фун­кци­ях, выпол­няющих код, таких как eval или exec). Цель taint analysis — пре­дот­вра­тить исполь­зование таких дан­ных без дол­жной обра­бот­ки.

При­мер прос­того taint-пра­вила в Semgrep
rules:
- id: taint-example
pattern-sources:
- pattern: request.get(...)
pattern-sinks:
- pattern: exec(...)
message: "Potentially unsafe data is passed to exec."
languages: [python]
severity: error
mode: taint

Здесь важ­но понимать основные кон­цепции:

В этом при­мере Semgrep будет искать, где дан­ные, получен­ные через request.get(...), переда­ются в фун­кцию exec(...). Такая схе­ма при­вела бы к появ­лению уяз­вимос­ти, которую может обна­ружить и про­экс­плу­ати­ровать зло­умыш­ленник.

Пишем шаблоны для Semgrep

Broken Object Level Authorization

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

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

Соз­дадим файл templates/bola.yaml и запишем в него сле­дующее:

rules:
- id: python-bola
languages: [python]
message: Access to data without verifying that it belongs to the user
severity: ERROR
patterns:
- pattern-not-inside: |
$USER = User.query.filter_by(...).first()
...
$OBJ.query.filter_by(...,user=$USER,...)
- pattern: $OBJ.query.filter_by(...)
- metavariable-regex:
metavariable: $OBJ
regex: Book

Раз­берем пра­вило:

Мы ука­зали метод query.filter_by и про­чие явные конс­трук­ции язы­ка в пра­виле потому, что мы зна­ем, что имен­но так они при­меня­ются в коде нашего при­ложе­ния.

Broken Function Level Authorization

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

Опять же из понима­ния устрой­ства нашей сис­темы кон­тро­ля дос­тупа мы зна­ем, что в кон­трол­лерах, обра­баты­вающих зап­росы к дан­ным поль­зовате­лей, дол­жна вызывать­ся фун­кция token_validator, отве­чающая за авто­риза­цию. Соз­дадим файл templates/bfla.yaml и запишем в него пра­вило:

rules:
- id: python-bfla
languages: [python]
message: Access to user data without authorization
severity: ERROR
patterns:
- pattern-either:
- pattern-inside: |
def $FUNC(...):
...
$RETURN = $RESPONSE(...,$OBJ,...)
...
return $RETURN
...
- pattern-inside: |
def $FUNC(...):
...
return $RESPONSE(...,$OBJ,...)
...
- pattern-not-inside: |
def $FUNC(...):
...
token_validator(...)
...
- metavariable-regex:
metavariable: $OBJ
regex: .*User\\.

Раз­берем пра­вило:

Мы исполь­зуем перемен­ные $FUNC и $RESPONSE, что­бы най­ти кон­трол­леры и фун­кции под­готов­ки отве­тов. Мы никак не огра­ничи­ваем их наз­вания, поэто­му Semgrep най­дет все. Если бы мы хотели най­ти толь­ко кон­трол­леры, наз­вания которых начина­ются с get, мы бы уточ­нили перемен­ную $FUNC в metavariable-regex.

SQL Injection

Од­на из самых ста­рых, тем не менее до сих пор акту­аль­ных и опас­ных уяз­вимос­тей — это SQL-инъ­екция. Под­готовим пра­вило, про­веря­ющее, что в нашем при­ложе­нии нет небезо­пас­ных зап­росов к базе дан­ных — таких, в которые попада­ет поль­зователь­ский ввод. Эту задачу мож­но решить с помощью taint analysis. Соз­дадим файл templates/sqli.yaml и запишем сле­дующее:

rules:
- id: tainted-python-sqli
languages: [python]
severity: ERROR
message: User-controlled data from a request is passed to db.execute
mode: taint
pattern-sinks:
- patterns:
- pattern: $QUERY
- pattern-inside: $DB.execute(...,$QUERY,...)
pattern-sources:
- patterns:
- pattern: $ARG
- pattern-inside: |
def $HANDLER(...,$ARG,...):
...

Раз­берем пра­вило:

В нашем слу­чае мы ищем пути, в которых дан­ные из кон­трол­лера могут попасть в вызов метода execute.

Сканируем стенд

Что­бы запус­тить ска­ниро­вание Semgrep с нашими шаб­лонами, дос­таточ­но прос­то перей­ти в дирек­торию с исходным кодом тес­тиру­емо­го при­ложе­ния и выпол­нить коман­ду

semgrep --config templates

Здесь templates — дирек­тория, в которой находят­ся наши шаб­лоны.

В резуль­тате запус­ка наших кас­томных пра­вил мы обна­ружи­ли пять уяз­вимос­тей в при­ложе­нии.

Ес­ли нажать наз­вания фай­лов из вывода Semgrep, то мы ока­жем­ся в мес­те с уяз­вимым кодом и смо­жем под­твер­дить уяз­вимость.

SQL-инъекция
SQL-инъ­екция

Выводы

Итак, в этой статье мы:

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