Tak wygląda karta uruchomienia i omówienie raportu
Raport z audytu ma 40 znalezisk, w tym 2 oznaczone jako krytyczne. Z omówienia wiesz, że dziś naprawiasz 16, jedno krytyczne może poczekać do końca miesiąca, jedno to prawdopodobnie fałszywy alarm, a zależności nikt nie sprawdził. Poniżej trzy dokumenty w kolejności, w jakiej powstają: Twój formularz, karta uruchomienia i omówienie.
To jest przykład. Audyt odpaliliśmy na Ledgerly, naszej aplikacji testowej z celowo wstawionymi błędami, a odpowiedzi z formularza wymyśliliśmy na potrzeby przykładu. Raport, ścieżki plików i numery linii są prawdziwe, z przebiegu z 2 listopada 2026.
Pytania są te same, które dostaniesz po zakupie. Odpowiedzi wymyśliliśmy pod prawdziwy stack Ledgerly z jego package.json.
1. Po co Ci ten audyt i co zrobisz z wynikiem?
Za miesiąc włączamy płatne plany. Pierwszy większy klient, biuro rachunkowe, pyta, jak chronimy dane jego klientów. Chcę wiedzieć, co naprawić przed startem, i mieć coś konkretnego do pokazania temu klientowi.
2a. Na czym stoi Twoja apka?
JavaScript / TypeScript (Node, Next.js)
2b. Framework, baza, logowanie i hosting w dwóch zdaniach
Next.js 16 (App Router) z tRPC i Hono, Postgres przez Drizzle, logowanie Better-Auth z organizacjami, płatności Stripe, pliki w R2, asystent AI na Anthropic przez AI SDK. Produkcja jako obraz Dockera, podglądy pull requestów na Vercelu, zadania w tle na pg-boss.
3a. Ile plików ma repo? (git ls-files | wc -l)
90
3b. Czy to monorepo?
Nie
4. Co w Twojej apce ma najwięcej do stracenia? Wybierz do 3.
Płatności · Dane innych użytkowników · Funkcje AI (klucze, koszty)
5a. Narzędzie i model
Claude Code przez API, Opus 5.5
5b. Ile maksymalnie chcesz wydać na jeden przebieg (USD)?
20
6. Komenda, którą odpalasz testy
pnpm test
7a. Repo jest
Prywatne
8. Kiedy odpalisz audyt?
W tym tygodniu
Opcjonalnie: lista zależności
next 16.3.0, better-auth 1.6.27, drizzle-orm 0.45.2, @trpc/server 11.8.0, hono 4.12.25, stripe 22.3.1, ai 6.0.40, @ai-sdk/anthropic 3.0.12, pg-boss 10.1.0, @aws-sdk/client-s3 3.890.0, resend 6.1.0, svix 1.76.1, zod 4.1.5 i 10 innych
Poza przykładem: liczbę plików zmierzyliśmy naprawdę. git ls-files na katalogu aplikacji testowej daje 90, tyle samo co zliczenie plików bez node_modules.
Krok 2 · do 2 dni roboczych od formularza
Karta uruchomienia
Dostajesz ją mailem. Mówi, jaką komendę wpisać, dlaczego z tymi flagami, ile to mniej więcej kosztuje i co mi potem odesłać.
Karta uruchomienia: Ledgerly
31.10.2026. Przygotowana na podstawie Twojego formularza z 30.10.2026. Jeśli coś się nie zgadza, odpisz na tego maila, zanim odpalisz audyt.
1. Co sprawdzimy i dlaczego
Z formularza: za miesiąc startują płatne plany, a klient pyta, jak chronicie dane jego klientów. Dlatego w omówieniu najpierw dostaniesz listę rzeczy do naprawy przed startem, ułożoną według pilności, a osobno to, co raport potwierdził jako bezpieczne, z miejscem w kodzie, do rozmowy z tym klientem.
Twój stack: Next.js 16 (App Router), tRPC i Hono, Postgres z Drizzle, Better-Auth z organizacjami, Stripe, AI SDK z Anthropic, pg-boss. Profil skilla: JavaScript/TypeScript (Node), flaga --stack js. Ten profil zna wszystkie te biblioteki. Czego nie pokrywa: skanery reguł (semgrep dla kodu, zizmor dla workflowów GitHuba, hadolint i trivy dla Dockerfile) działają tylko wtedy, gdy masz je zainstalowane albo działa docker. Bez nich raport wpisze je jako niewykonane, a workflowy i Dockerfile zostaną przeczytane tylko przez model.
Na co szczególnie patrzymy w Twoim stacku:
server actions ("use server") i route handlery, które biorą id z requestu i nie sprawdzają, czy rekord należy do organizacji zalogowanego użytkownika;
ścieżki, których nie obejmuje matcher w proxy.ts: sprawdzenie logowania tylko w layoucie albo na stronie nie chroni /api;
webhook Stripe: czy podpis jest sprawdzany, zanim cokolwiek zapisze się w bazie;
Better-Auth: dodatkowe pola użytkownika (np. rola), które da się ustawić przy rejestracji, i ustawienia ciasteczek sesji;
narzędzia asystenta AI: czy model może sam wybrać organizację, czy narzędzia, które coś zmieniają, wymagają potwierdzenia, i czy model oraz limit tokenów ustawia serwer, a nie request;
zmienne z prefiksem NEXT_PUBLIC_, które trafiają do przeglądarki.
Priorytet według Twojej odpowiedzi na pytanie 4: płatności, dane innych użytkowników, funkcje AI.
2. Przed startem (5 minut)
Claude Code przez API, limit 20 USD. Szacowany koszt tego przebiegu: 11,71–18 USD, czas około 32 minut. Podstawa: dwa pełne przebiegi Claude Code z Opus 5.5 na naszej aplikacji testowej z tym samym stackiem (Next.js, Drizzle) i podobnej wielkości, w październiku 2026: 11,71 USD i 32 minuty oraz 18 USD.
Node 20 lub nowszy, repo sklonowane lokalnie, gałąź main, czyste drzewo (git status nic nie pokazuje).
Skanery: osv-scanner i gitleaks w PATH albo działający docker. Bez nich raport nie sprawdzi zależności ani sekretów w historii gita. Masz workflowy GitHuba i Dockerfile, więc jeśli możesz, dołóż też zizmor, hadolint i trivy.
Testy: zainstaluj zależności tak, jak robisz to na co dzień, żeby testy regresyjne, które napisze skill, dało się uruchomić przez pnpm test. Jeśli testów jest mało, skill zapisze to jako lukę, a nie błąd.
Skill nie startuje Twojej apki i nie zmienia kodu poza katalogiem .security-audit/. Wynik ląduje w tym katalogu. Zanim zrobisz commit, sprawdź, że jest w .gitignore.
3. Uruchomienie
W katalogu repo, w świeżej sesji Claude Code:
/security-audit --stack js
Dlaczego tak: bez --scope, bo 90 plików to małe repo i pełny przebieg mieści się w Twoim limicie 20 USD. --stack js tylko przypina profil; skill i tak wykryłby go z package.json.
Czas: około 32 minut. Skill pokazuje postęp. Nie przerywaj w fazie weryfikacji.
4. Jeśli coś pójdzie nie tak
Koszt zbliża się do limitu: przerwij i odpisz. Dostaniesz węższy zakres, np. --scope payments,ai.
Błąd przy instalacji albo nieznana flaga: wklej komunikat w odpowiedzi na tego maila. Odpowiadam do 2 dni roboczych.
Testy nie przechodzą przed audytem: to nie blokuje audytu, ale wynik testów regresyjnych będzie niepełny. Napisz, jak odpalasz testy u siebie.
5. Co wysłać do omówienia
Plik .security-audit/report.md, w odpowiedzi na tego maila. Raport zawiera ścieżki plików i fragmenty kodu. Zanim wyślesz, usuń hasła, klucze i dane Twoich użytkowników (to obowiązkowe) i wytnij fragmenty kodu, których nie chcesz pokazywać. Nie wycinaj sekcji „Not Assessed”: bez niej nie powiem Ci, czego audyt nie sprawdził.
Omówienie dostaniesz do 3 dni roboczych od raportu.
Bartek
Krok 3 · do 3 dni roboczych od raportu
Omówienie raportu
To główna część usługi. Raport układa znaleziska według wagi, ja według tego, co wiem o Twojej apce z formularza. W tym przykładzie znalezisko krytyczne (config-301) spadło na „w tym miesiącu”, bo repo jest prywatne, średnie (exposure-101) trafiło na dziś, bo otwiera oba łańcuchy, a jedno (rate-limit-408) to prawdopodobnie fałszywy alarm. Każde znalezisko ma plik i linię z raportu.
Omówienie audytu: Ledgerly
4.11.2026. Raport z przebiegu 2.11.2026 (commit a03d8d4), skill security-audit, profil JavaScript/TypeScript (Node), całe repo bez --scope, koszt 12,80 USD i 22 minuty. Szkic przygotował model AI (Claude), sprawdził, poprawił i podpisał Bartek Filipiuk. Na podstawie raportu, bez dostępu do kodu.
1. W jednym akapicie
Raport ma 40 znalezisk. 38 uznaję za prawdziwe, jedno za prawdopodobnie fałszywy alarm, a jednego nie rozstrzygnę bez Twojej odpowiedzi. Na dziś zostawiam 16 znalezisk w czterech paczkach: sekrety, które już są poza Twoją kontrolą, i dziury, które wykorzysta ktoś z darmowym kontem, a część nawet bez konta. Zacznij od wymiany kluczy, to kwestia minut, potem webhook Stripe. Najgroźniej w raporcie wygląda workflow podglądów pull requestów (oznaczony jako krytyczny), ale przy prywatnym repo to robota na ten miesiąc, nie na dziś. Audyt nie sprawdził zależności i nie uruchomił ani jednego testu, więc każda poprawka zaczyna się od testu, który ma paść. W tym tygodniu: paczki A–D poniżej, w tej kolejności.
2. Napraw dziś
Klucz do tej sekcji: sekrety i kod, które już są poza Twoją kontrolą, oraz błędy, które wykorzysta ktoś bez konta albo z darmowym kontem, bez udziału ofiary. Skutek: cudze dane, Twoje pieniądze albo uprawnienia admina. Kolejność paczek: A, B, C, D. Każda paczka ma prompt dla Twojego agenta; wklej go w świeżej sesji w katalogu repo.
AKlucze i skrypt instalacyjny
Najpierw Ty, bez agenta: wymień klucz Stripe w panelu Stripe (Developers, API keys, Roll key), drugi klucz z tego samego pliku .env u jego wystawcy i klucz Anthropic w Anthropic Console (API keys). Dopiero potem kod.
exposure-306highprawdziwe
Klucz Stripe i drugi klucz API w historii gita
.env w commicie a1b7c73 (usunięty z drzewa, został w historii)
Wyciek jest pewny: gitleaks znalazł oba w historii, a każdy klon repo je ma. Czy klucz Stripe jest aktywny i czy to klucz live, raport celowo nie sprawdzał; zobaczysz to tylko w panelu Stripe. Wymiana kosztuje minuty, więc nie czekaj na tę odpowiedź.
exposure-305highprawdziwe
Klucz API Anthropic trafia do przeglądarki
src/lib/ai-client.ts:3-6
Zmienna z prefiksem NEXT_PUBLIC_ z definicji ląduje w JavaScripcie strony, więc każdy, kto założy konto i otworzy asystenta, może ją skopiować i wydawać z Twojego konta.
config-302highprawdziwe
postinstall pobiera skrypt z internetu i uruchamia go w powłoce
package.json:14
Kto przejmie tamten serwer albo domenę, uruchomi swój kod na każdej instalacji: u Ciebie, w CI i w obrazie produkcyjnym. Usunięcie to jedna linia.
Prompt naprawy dla agenta (paczka A)
Naprawiamy trzy znaleziska z .security-audit/report.md, w tej kolejności. Klucze u dostawców już wymieniłem, Ty zajmij się kodem. Przy punktach 1 i 2 najpierw test, potem poprawka.
1. exposure-305, src/lib/ai-client.ts:3-6. Przenieś .security-audit/tests/exposure-305.browser-key.test.ts do testów projektu i uruchom pnpm test. Test MUSI paść na obecnym kodzie; jeśli przechodzi, popraw test, nie kod. Potem usuń src/lib/ai-client.ts i NEXT_PUBLIC_ANTHROPIC_API_KEY z .env.example, a podpowiedź przypomnienia z src/components/assistant-panel.tsx przenieś do server action za requireOrg(), która używa serwerowego ANTHROPIC_API_KEY. Uruchom pnpm test: nowy test przechodzi.
2. config-302, package.json:14. Przenieś .security-audit/tests/config-302.postinstall.test.ts, uruchom: MUSI paść. Usuń skrypt postinstall. Jeśli to narzędzie jest potrzebne do budowania, zatrzymaj się i zapytaj mnie, zanim cokolwiek dodasz. Test przechodzi.
3. exposure-306. Nie ruszaj historii gita. Dodaj do .github/workflows/ci.yml krok gitleaks, który blokuje push z sekretem.
Na koniec: pnpm test, lista zmienionych plików i diff. Nie zmieniaj niczego poza tymi miejscami.
BWebhook Stripe bez podpisu
auth-101criticalprawdziwe
Webhook Stripe przyjmuje niepodpisane zdarzenia
src/app/api/webhooks/stripe/route.ts:6-41
Nikt nie sprawdza podpisu, więc każdy, bez konta, może zmienić zapis subskrypcji dowolnej organizacji: aktywować plan albo go anulować. Raport pokazuje, że w tym samym repo webhook Resend sprawdza podpis poprawnie, więc wzorzec już masz. README obiecuje weryfikację, której nie ma.
Prompt naprawy dla agenta (paczka B)
Naprawiamy auth-101 z .security-audit/report.md: webhook Stripe w src/app/api/webhooks/stripe/route.ts:6-41 nie sprawdza podpisu.
Najpierw test: przenieś .security-audit/tests/auth-101.stripe-webhook-signature.test.ts do testów projektu i uruchom pnpm test. Test MUSI paść na obecnym kodzie. Jeśli przechodzi, test niczego nie sprawdza: popraw test, nie kod.
Poprawka: czytaj surowe body przez request.text(), weryfikuj je przez stripe.webhooks.constructEvent z nagłówkiem stripe-signature i STRIPE_WEBHOOK_SECRET i zwracaj 400 przy błędzie, zanim cokolwiek zapiszesz. Plan przyjmuj tylko z listy znanych planów (pro, team). Wzorzec weryfikacji przed zapisem jest w src/app/api/webhooks/resend/route.ts. Popraw też zdanie o weryfikacji webhooków w README.md, jeśli po zmianie nadal się nie zgadza.
Uruchom pnpm test: nowy test przechodzi, reszta bez zmian. Pokaż diff.
CRola admina i panel statystyk
Kolejność ma znaczenie: poprawka auth-103 sprawdza rolę admina, a dziś tę rolę może sobie nadać każdy (auth-104). Najpierw auth-104.
auth-104highprawdziwe
Rolę admina platformy użytkownik może ustawić sobie sam
src/lib/auth.ts:27-32
Pole role w dodatkowych polach Better-Auth nie ma input: false, a według dokumentacji biblioteki takie pole przyjmuje wartość z rejestracji. Raport opiera to na dokumentacji, bo nie czytał kodu biblioteki; test z paczki rozstrzygnie to na Twojej wersji.
auth-103highprawdziwe
/api/admin/stats bez logowania: liczby platformy i e-maile 25 ostatnich rejestracji
src/app/api/admin/stats/route.ts:5-25
Sprawdzenie admina jest tylko na stronie /admin, a matcher w proxy.ts nie obejmuje /api/admin. Endpoint odpowiada każdemu z internetu.
Prompt naprawy dla agenta (paczka C)
Dwie poprawki z .security-audit/report.md, w tej kolejności, bo druga opiera się na pierwszej. Przy każdej najpierw test, który MUSI paść na obecnym kodzie; jeśli przechodzi, popraw test, nie kod.
1. auth-104, src/lib/auth.ts:27-32. Przenieś .security-audit/tests/auth-104.role-not-client-writable.test.ts do testów projektu, uruchom pnpm test: ma paść. Ustaw input: false dla role (i dla emailDigest, jeśli ma się zmieniać tylko w ustawieniach). Rolę admina nadawaj tylko po stronie serwera. W requireAdmin (src/lib/session.ts:22-25) czytaj rolę z bazy, nie z sesji. Test przechodzi.
2. auth-103, src/app/api/admin/stats/route.ts:5-25. Przenieś .security-audit/tests/auth-103.admin-stats-auth.test.ts, uruchom: ma paść. Dodaj w samym handlerze sprawdzenie sesji i roli admina: 401 bez sesji, 403 bez roli. Nie polegaj na proxy.ts ani na stronie /admin. Test przechodzi.
Na koniec pnpm test i diff. Nie zmieniaj niczego poza tymi plikami.
Po wdrożeniu sprawdź w bazie, czy w tabeli user ktoś poza Tobą ma rolę admin. Jeśli tak, ktoś mógł ją sobie nadać.
DGranica między organizacjami
Jedna przyczyna, dziesięć znalezisk: organizacja albo id rekordu przychodzi z requestu lub od modelu i nikt nie sprawdza, czy należy do zalogowanego użytkownika. To dokładnie to, o co pyta Twój klient. Raport składa te miejsca w dwa łańcuchy (chain-901 i chain-902): publiczna strona projektu zdradza id organizacji, projektu i klucze plików, a pozostałe miejsca te id przyjmują. Zacznij od exposure-101, bo ta jedna poprawka przerywa pierwszy krok obu łańcuchów, ale napraw wszystkie, bo każde miejsce osobno też jest dziurą.
exposure-101mediumprawdziwe
Publiczna strona projektu wysyła anonimom cały wiersz projektu
src/app/p/[slug]/page.tsx:7-12
Strona pokazuje tylko nazwę i opis, ale do komponentu klienckiego idzie cały rekord: notatki wewnętrzne, budżet, e-mail kontaktowy, id organizacji i klucze plików. Raport daje medium, ja daję dziś, bo to otwiera oba łańcuchy.
auth-107highprawdziwe
Eksport klientów bierze organizację z parametru ?org=
src/app/api/export/route.ts:6-23
Każde zalogowane konto, także świeże i bez organizacji, pobierze listę klientów cudzej organizacji z e-mailem, telefonem i NIP-em. Razem z exposure-101 to chain-901.
auth-106highprawdziwe
Narzędzie AI searchInvoices przyjmuje id organizacji od modelu
src/server/ai/tools.ts:7-36
Jedyną granicą jest zdanie w prompcie systemowym, a prompt nie jest zabezpieczeniem. To, czy model posłucha, nie ma znaczenia: narzędzie w ogóle nie powinno przyjmować organizacji.
chain-901highprawdziwe
Łańcuch: publiczna strona projektu, eksport klientów i wyszukiwanie faktur
src/app/p/[slug]/page.tsx:7-12
Nie osobna poprawka, tylko suma exposure-101, auth-107 i auth-106. Zamyka się razem z nimi.
upload-401highprawdziwe
Klucz pliku przy uploadzie wybiera klient, bez prefiksu organizacji
src/app/api/uploads/route.ts:13,23-37
Członek dowolnej organizacji może nadpisać albo odczytać cudzy plik w buckecie, jeśli zna jego klucz, a klucze plików z publicznych projektów zdradza exposure-101.
auth-403mediumprawdziwe
Upload przyjmuje dowolny projectId
src/app/api/uploads/route.ts:10,29-37
Ta sama trasa co upload-401, ta sama poprawka: przed podpisaniem sprawdź, że projekt należy do organizacji z sesji.
auth-108highprawdziwe
tRPC project.update zmienia projekt po samym id
src/server/trpc/routers/project.ts:12-27
Członek innej organizacji może przepisać cudzy projekt, przełączyć go na publiczny i odczytać notatki wewnętrzne z odpowiedzi. Sąsiednia procedura list w tym samym routerze filtruje po organizacji poprawnie.
chain-902highprawdziwe
Łańcuch: publiczna strona, upload i edycja projektu
src/app/p/[slug]/page.tsx:7-12
Suma exposure-101, upload-401, auth-403 i auth-108. Skutek, którego najbardziej bym się bał: podmiana pliku, który członkowie cudzej organizacji potem pobierają jako swój.
auth-102highprawdziwe
Server action getInvoice bez logowania i bez organizacji
src/app/(app)/invoices/actions.ts:1,10-15
Akcja jest importowana przez komponent kliencki, więc jest publicznym endpointem. Funkcja obok w tym samym pliku wywołuje requireOrg(), ta nie. Ryzyko trochę zmniejsza to, że id faktur nie da się zgadnąć z samego kodu.
auth-105highprawdziwe
Narzędzie AI voidInvoice anuluje fakturę dowolnej organizacji, bez potwierdzenia
src/server/ai/billing-tools.ts:7-20
ctx.orgId jest przekazywane, ale to narzędzie go nie używa. Bliźniacze narzędzie markInvoicePaid w tym samym pliku robi to dobrze i ma needsApproval.
Prompt naprawy dla agenta (paczka D)
Naprawiamy granicę między organizacjami, znaleziska z .security-audit/report.md. Zasada dla całej paczki: organizację zawsze bierzemy z sesji, nigdy z requestu ani od modelu, a każde zapytanie o rekord organizacji ma w where orgId z sesji.
Kolejność:
1. exposure-101, src/app/p/[slug]/page.tsx:7-12: do PublicProjectView przekazuj osobny obiekt z name, description i nazwami plików, nie cały wiersz z bazy.
2. auth-107, src/app/api/export/route.ts:6-23: ignoruj parametr org, używaj organizacji z sesji, eksport tylko dla roli owner albo admin.
3. auth-106, src/server/ai/tools.ts:7-36: usuń orgId ze schematu wejścia searchInvoices, używaj ctx.orgId.
4. upload-401 i auth-403, src/app/api/uploads/route.ts:13,23-37 oraz :10,29-37: klucz pliku generuje serwer w postaci orgId/projectId/uuid, klucz od klienta ignoruj; przed podpisaniem sprawdź, że projectId należy do organizacji z sesji, inaczej 404.
5. auth-108, src/server/trpc/routers/project.ts:12-27: update z warunkiem na id i ctx.orgId, jawna lista zwracanych kolumn, zmiana isPublic tylko dla owner albo admin.
6. auth-102, src/app/(app)/invoices/actions.ts:1,10-15: requireOrg() i warunek na orgId, tak jak w invoice.byId.
7. auth-105, src/server/ai/billing-tools.ts:7-20: warunek na ctx.orgId, needsApproval: true, narzędzie tylko dla owner albo admin.
Przy każdym punkcie osobno: najpierw test. Jeśli w .security-audit/tests/ jest test dla tego id (są dla auth-102, auth-105, auth-106, auth-107, auth-108 i upload-401), przenieś go do testów projektu. Jeśli nie ma, napisz test, który wywołuje kod z id rekordu innej organizacji i sprawdza, że nie dostaje ani nie zmienia cudzych danych. Uruchom pnpm test: nowy test MUSI paść na obecnym kodzie; jeśli przechodzi, popraw test, nie kod. Dopiero potem poprawka i pnpm test na zielono. Jeden commit na punkt.
Nie zmieniaj niczego poza tymi miejscami. Jeśli poprawka wymaga zmiany gdzie indziej, zatrzymaj się i zapytaj.
3. Napraw w tym miesiącu
Prawdziwe, ale wymagają warunku: zalogowany użytkownik musi wejść na obcą stronę, coś musi działać na produkcji albo skutek to koszt, a nie wyciek. Kolejność od góry.
auth-110 i auth-111: CORS odbija każdy Origin, a ciasteczko sesji ma SameSite=None
Wymaga, żeby zalogowany użytkownik wszedł na obcą stronę, a część przeglądarek i tak nie wyśle ciasteczka. Naprawa: lista dozwolonych originów, SameSite=Lax, sprawdzanie Origin w route handlerach, które coś zmieniają.
injection-203: odbiornik postMessage bez sprawdzenia nadawcy przekierowuje na adres z wiadomości
src/components/payment-method-frame.tsx:13-22
Też wymaga wizyty na obcej stronie, ale skutek jest poważny: skrypt w Twojej domenie. Naprawa: sprawdzaj event.origin i event.source tak jak src/components/signature-frame.tsx:22-25.
config-308: brak nagłówków frame-ancestors, COOP i CSP
next.config.ts:3-13
Sam nie jest dziurą, ale otwiera drogę injection-203. Naprawa: headers() w next.config.ts.
auth-109: cotygodniowy digest zaległych faktur idzie do wszystkich użytkowników platformy
src/server/jobs/digest.ts:28-43
Działa tylko, jeśli na produkcji chodzi proces worker (pytanie 2 niżej). Jeśli chodzi, przenieś na dziś. Naprawa: odbiorcy tylko z członków danej organizacji.
config-301: workflow podglądów buduje kod z pull requesta z sekretami
.github/workflows/preview.yml:3-5,7-9,15-32
Raport daje critical, ale warunkiem są pull requesty od osób z zewnątrz, a Twoje repo jest prywatne. Jeśli je otworzysz albo wpuścisz zewnętrznego współpracownika, przenieś na dziś. Naprawa: build w zwykłym pull_request bez sekretów, wdrożenie podglądu osobno, za zatwierdzeniem.
config-307: Dockerfile zapisuje sekrety produkcyjne w obrazie
Dockerfile:14-22
Skutek zależy od tego, kto może pobrać obraz (pytanie 3). Naprawa: usuń ARG i ENV z sekretami, wstrzykuj je przy starcie kontenera i wymień te, które już były w wypchniętym obrazie.
injection-202: testWebhook pobiera dowolny URL i zwraca odpowiedź
src/server/trpc/routers/integration.ts:18-28
Prawdziwe, ale skutek zależy od hostingu (pytanie 3). Naprawa: tylko https, blokada adresów lokalnych i prywatnych, bez śledzenia przekierowań, zwracaj sam status.
rate-limit-102: wysyłka kodu SMS bez logowania i bez limitu
src/app/api/phone/send-code/route.ts:7-22
Nic nie wycieka, ale każde wywołanie kosztuje. Dziś ustaw alert wydatków u dostawcy SMS. Naprawa: limit na numer i adres IP, CAPTCHA, lista dozwolonych krajów.
rate-limit-101: model, limit tokenów i liczbę kroków asystenta wybiera request
src/app/api/assistant/billing/route.ts:6-22
Koszt, nie wyciek. Naprawa: walidacja body przez zod, model ustawiony na serwerze, górne limity i budżet na organizację.
rate-limit-103: wywołania modelu bez budżetu na organizację (3 miejsca)
src/app/api/chat/route.ts:11-19
Koszt pojedynczego wywołania jest ograniczony, liczba wywołań nie. Ten sam licznik co przy rate-limit-101.
rate-limit-411: zaproszenia do zespołu bez limitu
src/app/api/team/invite/route.ts:14-41
Grozi reputacji domeny nadawczej, a z nią mailom weryfikacyjnym. Naprawa: dzienny limit na organizację i brak duplikatów.
upload-402: podpisany upload bez limitu rozmiaru i typu pliku
src/app/api/uploads/route.ts:9-14,23-27
Koszt przechowywania. Naprawa przy okazji paczki D: lista typów i ContentLength w podpisie.
auth-113: cache zużycia bez organizacji w kluczu
src/server/queries/dashboard.ts:20-33
Pokazuje cudze sumy zużycia przez 10 minut; to dane zbiorcze, nie osobowe. Naprawa: orgId jako argument funkcji w cache.
exposure-415: usunięcie organizacji zostawia jej pliki w buckecie
Raport sam zaznacza, że w repo nie ma kodu, który zapisuje notatki faktur, więc nie wiem, kto je pisze (pytanie 1). Naprawa jest tania niezależnie od odpowiedzi: renderuj odpowiedź modelu jako tekst.
injection-212: pola użytkownika wstawiane do HTML maili bez escapowania
src/lib/auth.ts:17-25
Treść i linki w mailu z Twojej domeny, bez wykonania skryptu. Naprawa: istniejący helper escapeHtml w obu szablonach.
concurrency-405: webhook Stripe bez idempotencji
src/app/api/webhooks/stripe/route.ts:7-38
Zostaje po paczce B. Naprawa: zapisuj event.id z unikalnym indeksem i pomijaj powtórki.
concurrency-410: startCheckout bez sprawdzenia istniejącej subskrypcji, dla każdego członka
src/app/(app)/settings/billing/actions.ts:10-22
Wymaga drugiej zapłaconej subskrypcji. Naprawa: tylko owner i admin, przy aktywnej subskrypcji portal klienta zamiast checkoutu.
business-logic-101: dowolne przejścia statusu faktury
src/app/(app)/invoices/actions.ts:19-27
W obrębie własnej organizacji. Naprawa: warunek na obecny status w tym samym zapytaniu.
injection-213: przekierowanie po logowaniu da się wyprowadzić poza domenę
src/components/login-form.tsx:18-19
Pomaga w phishingu, nic nie wycieka. Naprawa: porównanie origin przez new URL.
Prompt do każdego znaleziska z tej sekcji, po jednym naraz:
Naprawiamy <id> z .security-audit/report.md. Jeśli w .security-audit/tests/ jest test dla tego id, przenieś go do testów projektu; jeśli nie, napisz test, który pokazuje błąd. Uruchom pnpm test: test MUSI paść na obecnym kodzie; jeśli przechodzi, popraw test, nie kod. Potem poprawka według „How to fix” z raportu, pnpm test na zielono, pokaż diff.
4. Zignoruj albo zaakceptuj świadomie
rate-limit-408: sendAiReminder bez limitu prawdopodobnie fałszywy alarm
Brak limitu to fakt, ale to nie podatność. Adresat to klient Twojej organizacji z bazy, a zapytanie filtruje po organizacji z sesji. Treść od modelu jest escapowana, co raport sam potwierdza w sekcji bezpiecznych miejsc. Członek organizacji i tak może pisać do własnych klientów. Problemem byłoby to dopiero wtedy, gdyby adresata wybierał użytkownik albo model.
injection-207: eksport CSV nie neutralizuje formuł arkusza prawdziwe, zaakceptuj
src/app/api/export/route.ts:26-28,38-40
Ktoś musiałby wpisać formułę w dane klienta, a w repo nie ma kodu, który te dane zapisuje. Arkusze ostrzegają przed treścią zewnętrzną. Wróć do tego, jeśli Twoi użytkownicy będą sami wpisywać dane swoich klientów.
logging-309: brak logów zdarzeń bezpieczeństwa prawdziwe, zaakceptuj na teraz
src/server/jobs/worker.ts:23-26
Nic nie otwiera, ale po incydencie nie odtworzysz, co się stało. Rozsądne do startu płatnych planów; wróć, gdy będziesz wdrażać monitoring.
Poza omówieniem: w kluczu odpowiedzi aplikacji testowej miejsce z rate-limit-408 jest celową pułapką na fałszywy alarm. To jedyna pułapka, w którą wpadł ten przebieg.
5. Co raport potwierdził jako bezpieczne
Do rozmowy z Twoim klientem, po naprawie paczki D. Każde z miejscem w kodzie z raportu:
webhook Resend sprawdza podpis na surowym body przed jakąkolwiek zmianą: src/app/api/webhooks/resend/route.ts:9-21;
klucze API są przechowywane jako hash i porównywane w stałym czasie: src/server/api/keys.ts:6-19;
pobranie dokumentu wymaga rekordu z Twojej organizacji, a link wygasa po 60 sekundach: src/app/api/documents/[id]/download/route.ts:15-30;
usunięcie danych klienta anonimizuje je w jednej transakcji i tylko dla owner albo admin: src/app/(app)/customers/actions.ts:12-40;
dzienny limit SMS na organizację liczony pod blokadą, bez podwójnych wysyłek: src/app/(app)/invoices/[id]/sms-actions.ts:23-36.
6. Czego audyt nie objął
Zależności. osv-scanner i docker były niedostępne, a zapasowy pnpm audit się wysypał. „No vulnerable packages reported” znaczy tu „nie sprawdzono”, nie „czysto”. Puść osv-scanner na pnpm-lock.yaml jeszcze w tym tygodniu, to kilka minut.
Testy. Żaden test nie został uruchomiony: nie było node_modules, a skill celowo nie instalował zależności, bo instalacja uruchomiłaby skrypt z config-302. Wszystkie znaleziska potwierdzono czytaniem kodu. Dlatego prompty wyżej zaczynają się od testu, który ma paść, wszędzie tam, gdzie da się go napisać.
Skanery reguł. semgrep, zizmor, hadolint i trivy nie ruszyły. Workflowy i Dockerfile czytał tylko model. Warto dorobić po paczce A.
Kod bibliotek. Które endpointy pluginu organizacji w Better-Auth wymagają jakiej roli, czy ciasteczko sesji ma HttpOnly i jak tRPC traktuje content-type przy żądaniach z innej strony: raport nie mógł tego przeczytać. Ciasteczko sprawdzisz w devtools po zalogowaniu.
Infrastruktura. Polityka bucketu R2, nagłówki i HSTS dodawane przez hosting, kopie zapasowe, logi wysyłek w Resend. Sekcja „Production checklist” w raporcie mówi, jak sprawdzić każdą z tych rzeczy u siebie.
Poza omówieniem: w Ledgerly zasialiśmy 26 błędów, a ten przebieg znalazł 25. Brakujący to podatna wersja Next.js, czyli dokładnie to, czego nie dało się znaleźć bez skanera zależności. Dlatego ta sekcja jest w każdym omówieniu.
7. Co wpiąć w CI
Do .github/workflows/ci.yml: osv-scanner na lockfile i gitleaks na każdym pull requeście, oba blokujące. Testy z .security-audit/tests/, które przeniesiesz przy paczkach A–D, lądują w zwykłym pnpm test, więc CI pilnuje, żeby poprawki nie wróciły. Po większej zmianie odpal lokalnie /security-audit --since last: skill sprawdzi tylko to, co się zmieniło, i pokaże, co zamknąłeś, a co wróciło. To ten sam raport, który odsyłasz mi do sprawdzenia poprawek.
8. Pytania do Ciebie
injection-201: kto i skąd wpisuje notatki faktur i opisy pozycji (formularz, import, API poza tym repo)? Od tego zależy, czy to jest dziura, czy tylko zła praktyka.
auth-109: czy proces worker działa na produkcji? Jeśli tak, przenoszę auth-109 na dziś.
config-307 i injection-202: gdzie stoi produkcja i czy obraz Dockera trafia do rejestru, do którego ma dostęp ktoś poza Tobą?
9. Co dalej
Na Twoim miejscu zrobiłbym w tym tygodniu paczki A–D, puścił osv-scanner i przysłał mi raport z --since last: sprawdzę, czy znaleziska z sekcji „Napraw dziś” są zamknięte. Jeśli chcesz, żebym wziął paczkę D i wpiął bramkę w CI u Ciebie, odpisz, wycenię po tym raporcie. Na raport z poprawkami masz 30 dni; w tym czasie możesz też przysłać jedną wiadomość z maksymalnie 5 pytaniami.
Bartek
To samo dla Twojego repo
Raport z tego przykładu to 40 znalezisk po angielsku. Omówienie to cztery paczki na ten tydzień z gotowymi promptami. Za 590 zł dostajesz kartę do 2 dni roboczych, omówienie do 3 dni roboczych od raportu, potem do 5 pytań i jedno sprawdzenie poprawek. Jeśli omówienie nie było warte tej kwoty, piszesz do 7 dni i oddaję całość.
Pełny raport, z którego powstało to omówienie (40 znalezisk, 30 miejsc potwierdzonych jako bezpieczne, 19 rzeczy, których audyt nie ocenił):