# Security Hardening Aplikacji AI-Generated — Checklista Implementacyjna dla Agenta

> **Dla agentów:** Implementuj zadania po kolei. Każde zadanie jest niezależnym commitem. Stosuj TDD: najpierw test zakończony niepowodzeniem, potem minimalna implementacja, potem test przechodzący. Zatrzymaj fazę jeśli jej testy nie przechodzą.

**Cel:** Narzucenie rygorystycznej architektury Zero-Trust na aplikacje stworzone przez generatory kodu (Vibe Coding). Kompletne zabezpieczenie bazy danych, serwera aplikacji (Next.js/Node.js), infrastruktury brzegowej (Cloudflare) oraz warstwy klienta.

**Stack:** TypeScript, Next.js (App Router), NeonDB (serverless PostgreSQL), Cloudflare Workers/Pages, Zod, Drizzle ORM / neon serverless driver.

---

## Zakres i granice

- Zadania 1–10: Warstwa danych — Bezpieczeństwo NeonDB/PostgreSQL (izolacja zapytań, RLS, connection security)
- Zadania 11–20: Architektura frameworka — Server Actions i klient React
- Zadania 21–30: Zarządzanie sesją, tokenami i autoryzacją
- Zadania 31–40: Kontrola zasobów, DoS, webhooki i integracje
- Zadania 41–50: Infrastruktura brzegowa — Cloudflare Workers, WAF, Edge policy

Nie wdrażaj na produkcję bez uruchomienia bramki testowej danej fazy. Nie commituj sekretów, connection stringów, plików `.tfstate` ani `.tfvars`.

---

## Struktura plików (nowe i modyfikowane)

```
scripts/security/
├── assert-public-artifacts.mjs      # Skanuje build pod wyciekami sekretów
├── assert-browser-source.mjs        # Sprawdza console.* i raw error w frontendzie
├── assert-security-control-matrix.mjs
├── public-artifact-policy.json      # Deny lista kluczy i hostów wewnętrznych

migrations/
├── 001_enable_rls.sql               # Aktywacja RLS i helper function app_user_id()
├── 002_optimize_rls_initplan.sql    # Optymalizacja podzapytaniami InitPlan
├── 003_column_projection.sql        # Widoki ograniczające eksponowane kolumny

src/lib/
├── env.ts                           # Typowany schemat Zod dla zmiennych środowiskowych
├── session.ts                       # Bezpieczne ciasteczka httpOnly, rotacja sesji
├── db.ts                            # Klient NeonDB z connection pooling
├── safe-action.ts                   # createSafeActionClient z walidatorem roli
├── rate-limit.ts                    # Sliding window limiter (Redis/KV)
├── public-error.ts                  # Maper błędów → publiczne kody
```

---

## Zadanie 1: Izolacja zapytań — każde zapytanie filtrowane przez user_id z sesji

**Cel:** NeonDB jest serverless PostgreSQL dostępnym wyłącznie przez tajny connection string po stronie serwera. AI generuje zapytania `SELECT * FROM orders WHERE id = $1` — bez warunku na właściciela. Każdy zalogowany użytkownik może podać cudze ID i pobrać cudze dane.

- [ ] **Krok 1: Dodaj `user_id` z sesji jako obowiązkowy warunek do każdego zapytania**

```ts
import { sql } from '@/lib/db';
import { getSession } from '@/lib/session';

// ŹLE — brak scopingu na użytkownika
export async function getOrder(orderId: string) {
  const { rows } = await sql`SELECT * FROM orders WHERE id = ${orderId}`;
  return rows[0];
}

// DOBRZE — user_id pochodzi z sesji serwera, nie z żądania
export async function getOrder(orderId: string) {
  const session = await getSession();
  if (!session?.user) throw new Error('Unauthorized');

  const { rows } = await sql`
    SELECT id, status, total, created_at
    FROM orders
    WHERE id = ${orderId} AND user_id = ${session.user.id}
  `;
  if (!rows[0]) throw new Error('Not found'); // nie ujawniaj "Forbidden"
  return rows[0];
}
```

- [ ] **Krok 2: Audyt wszystkich zapytań — grep po brakujących warunkach user_id**

```bash
# Znajdź zapytania bez user_id w warunku WHERE
grep -rn "FROM orders\|FROM posts\|FROM profiles" src/ | grep -v "user_id"
```

- [ ] **Krok 3: Test — użytkownik B nie może pobrać danych użytkownika A**

```ts
it('user cannot access another user order', async () => {
  const orderA = await createOrder(userA.id);
  const result = getOrderAsUser(orderA.id, userB.id); // wywołanie z userB
  await expect(result).rejects.toThrow('Not found');
});
```

- [ ] **Krok 4: Commit**

```bash
git add src/
git commit -m "security: scope all DB queries by session user_id"
```

---

## Zadanie 2: Zabezpieczenie connection string — baza danych tylko po stronie serwera

**Cel:** NeonDB connection string zawiera hasło i nigdy nie może trafić do przeglądarki, pliku po stronie klienta ani repozytorium Git.

- [ ] **Krok 1: Sprawdź czy connection string jest wyłącznie w zmiennych serwerowych**

```ts
// src/lib/env.ts — typowana weryfikacja przy starcie serwera
import { z } from 'zod';

const serverEnv = z.object({
  DATABASE_URL: z.string().url(), // NeonDB connection string
  // NIE używaj prefiksu NEXT_PUBLIC_ dla DATABASE_URL nigdy
}).parse(process.env);

export const env = serverEnv;
```

- [ ] **Krok 2: Dodaj skrypt wykrywający DATABASE_URL w bundle klienckim**

```js
// scripts/security/assert-public-artifacts.mjs
import { readFileSync, readdirSync } from 'fs';

const distFiles = readdirSync('.next/static/chunks', { recursive: true });
for (const file of distFiles) {
  const content = readFileSync(`.next/static/chunks/${file}`, 'utf8');
  if (content.includes('neon.tech') || content.includes('DATABASE_URL')) {
    throw new Error(`DB credentials leaked to client bundle: ${file}`);
  }
}
```

- [ ] **Krok 3: Sprawdź .gitignore pod .env**

```bash
echo ".env\n.env.local\n.env.production" >> .gitignore
git log --all --full-history -- .env # sprawdź czy .env nie było kiedyś commitowane
```

- [ ] **Krok 4: Commit**

```bash
git add scripts/security/ src/lib/env.ts
git commit -m "security: validate DB connection string is server-only"
```

---

## Zadanie 3: Row-Level Security w PostgreSQL — blokada na poziomie bazy

**Cel:** RLS w PostgreSQL (dostępne w NeonDB) to dodatkowa linia obrony — nawet jeśli kod aplikacji pominie filtr, baza sama odmówi dostępu.

- [ ] **Krok 1: Utwórz helper function przekazującą user_id z aplikacji do sesji PostgreSQL**

```sql
-- migrations/001_enable_rls.sql
-- Funkcja odczytuje user_id ustawiony przez aplikację przed każdym zapytaniem
CREATE OR REPLACE FUNCTION app_user_id() RETURNS UUID AS $$
  SELECT NULLIF(current_setting('app.current_user_id', TRUE), '')::UUID;
$$ LANGUAGE sql STABLE;

-- Włącz RLS na tabelach z danymi użytkowników
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;

-- Polityki dostępu
CREATE POLICY "User reads own orders" ON orders
  FOR SELECT USING (user_id = app_user_id());

CREATE POLICY "User modifies own orders" ON orders
  FOR ALL USING (user_id = app_user_id())
  WITH CHECK (user_id = app_user_id());
```

- [ ] **Krok 2: W kliencie DB — ustaw `app.current_user_id` przed każdym zapytaniem**

```ts
// src/lib/db.ts
import { neon } from '@neondatabase/serverless';

const sql = neon(env.DATABASE_URL);

export async function queryAsUser<T>(userId: string, query: TemplateStringsArray, ...values: unknown[]): Promise<T[]> {
  // Ustaw user_id dla tej transakcji w sesji PostgreSQL
  const { rows } = await sql.transaction([
    sql`SET LOCAL app.current_user_id = ${userId}`,
    sql(query, ...values),
  ]);
  return rows as T[];
}
```

- [ ] **Krok 3: Test — zapytanie bez ustawionego user_id zwraca puste wyniki**

```ts
it('RLS blocks query without user context', async () => {
  const { rows } = await sql`SELECT * FROM orders`; // bez SET LOCAL
  expect(rows).toHaveLength(0);
});
```

- [ ] **Krok 4: Commit**

```bash
git add migrations/001_enable_rls.sql src/lib/db.ts
git commit -m "security: enable RLS with app_user_id() helper"
```

---

## Zadanie 4: Optymalizacja RLS — unikaj wielokrotnego sprawdzania tożsamości

**Cel:** Błędna polityka RLS wywołuje `app_user_id()` dla każdego skanowanego wiersza (O(n)). Podzapytanie `(SELECT app_user_id())` jest obliczane raz (O(1)).

- [ ] **Krok 1: Użyj podzapytania w USING — tzw. InitPlan**

```sql
-- migrations/002_optimize_rls_initplan.sql

-- ŹLE (O(n) — funkcja wywoływana dla każdego wiersza):
CREATE POLICY "User read orders" ON orders
  FOR SELECT USING (user_id = app_user_id());

-- DOBRZE (O(1) — funkcja wywoływana raz):
CREATE POLICY "User read orders" ON orders
  FOR SELECT USING (user_id = (SELECT app_user_id()));
```

- [ ] **Krok 2: Zweryfikuj EXPLAIN ANALYZE przed i po**

```sql
EXPLAIN ANALYZE SELECT * FROM orders; -- sprawdź "InitPlan" w planie
```

- [ ] **Krok 3: Commit**

```bash
git add migrations/002_optimize_rls_initplan.sql
git commit -m "perf/security: optimize RLS with InitPlan subquery pattern"
```

---

## Zadanie 5: FORCE ROW LEVEL SECURITY — ochrona przed kontami administracyjnymi

**Cel:** Właściciel tabeli i role z atrybutem BYPASSRLS domyślnie omijają polityki RLS. `FORCE ROW LEVEL SECURITY` wymusza polityki nawet na tych kontach.

- [ ] **Krok 1: Nałóż FORCE RLS na krytyczne tabele**

```sql
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
ALTER TABLE user_profiles FORCE ROW LEVEL SECURITY;
ALTER TABLE payments FORCE ROW LEVEL SECURITY;
```

- [ ] **Krok 2: Utwórz dedykowaną rolę aplikacyjną bez BYPASSRLS**

```sql
-- Rola aplikacji bez możliwości obejścia RLS
CREATE ROLE app_role NOLOGIN;
GRANT CONNECT ON DATABASE mydb TO app_role;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_role;
-- Nie nadawaj BYPASSRLS ani SUPERUSER
```

- [ ] **Krok 3: Commit**

```bash
git commit -m "security: force RLS on critical tables, create restricted app_role"
```

---

## Zadanie 6: Eksplicytne projekcje kolumn — blokada wycieku haseł i kluczy API

**Cel:** ORM domyślnie zwraca wszystkie kolumny. Zapytanie `SELECT *` może zwrócić `password_hash`, `api_key`, `secret_token` — i przypadkowo wysłać je do klienta.

- [ ] **Krok 1: Nigdy nie używaj `SELECT *` w zapytaniach zwracanych do klienta**

```ts
// ŹLE — zwraca password_hash, api_key, reset_token:
const user = await sql`SELECT * FROM users WHERE id = ${userId}`;

// DOBRZE — tylko pola publiczne:
const user = await sql`
  SELECT id, name, email, created_at
  FROM users
  WHERE id = ${userId} AND id = (SELECT app_user_id())
`;
```

- [ ] **Krok 2: Utwórz widoki bazy z ograniczonymi kolumnami**

```sql
-- migrations/003_column_projection.sql
CREATE VIEW public_users AS
  SELECT id, name, email, created_at FROM users;

-- Aplikacja używa widoku zamiast tabeli bazowej
```

- [ ] **Krok 3: Dodaj ESLint rule blokujący `SELECT *` w plikach API**

---

## Zadanie 7: Weryfikacja własności przed każdą mutacją (IDOR/BOLA)

**Cel:** Nawet z poprawnym user_id w sesji — użytkownik może wysłać ID cudzego rekordu do mutacji. Każda modyfikacja wymaga jawnej weryfikacji własności.

- [ ] **Krok 1: Zawsze dołączaj `AND user_id = session.user.id` do UPDATE/DELETE**

```ts
export async function deletePost(postId: string) {
  const session = await getSession();
  if (!session?.user) throw new Error('Unauthorized');

  // Scoped DELETE — nie usunie nic jeśli post nie należy do użytkownika
  const { rowCount } = await sql`
    DELETE FROM posts
    WHERE id = ${postId} AND user_id = ${session.user.id}
  `;

  if (rowCount === 0) {
    throw new Error('Not found'); // celowo nie "Forbidden" — nie ujawniaj czy istnieje
  }
}
```

- [ ] **Krok 2: Test IDOR — użytkownik B nie może usunąć posta użytkownika A**

```ts
it('cannot delete another user post', async () => {
  const postA = await createPost(userA.id);
  await expect(deletePostAsUser(postA.id, userB.id)).rejects.toThrow('Not found');
  const stillExists = await getPost(postA.id);
  expect(stillExists).toBeDefined();
});
```

---

## Zadanie 8: Connection pooling i limity połączeń — ochrona przed wyczerpaniem DB

**Cel:** NeonDB serverless otwiera nowe połączenie przy każdym żądaniu. Bez poolingu, nagły ruch (lub atak) może wyczerpać limit połączeń bazy.

- [ ] **Krok 1: Skonfiguruj Neon connection pooler (PgBouncer wbudowany w NeonDB)**

```ts
// src/lib/db.ts
import { neon } from '@neondatabase/serverless';

// Użyj pooled connection string (endpoint z -pooler w nazwie hosta)
// np. postgresql://user:pass@ep-name-pooler.us-east-2.aws.neon.tech/db
const sql = neon(env.DATABASE_URL_POOLED);
export { sql };
```

- [ ] **Krok 2: Ustaw maksymalny czas oczekiwania na połączenie**

```ts
// Timeout zapobiega zawieszeniu requestu przy przeciążonej bazie
const sql = neon(env.DATABASE_URL_POOLED, {
  fetchOptions: { signal: AbortSignal.timeout(5000) } // 5 sekund max
});
```

- [ ] **Krok 3: Commit**

```bash
git commit -m "security: configure NeonDB connection pooler with timeout"
```

---

## Zadanie 9: Automatyczne testy izolacji danych — wielodostęp

**Cel:** Dane jednego użytkownika nigdy nie powinny być widoczne dla innego — nawet przy bezpośrednim podaniu ID.

- [ ] **Krok 1: Dodaj testy izolacji multi-user do CI/CD**

```ts
describe('Data isolation', () => {
  it('userA cannot read userB orders', async () => {
    const orderB = await createOrder(userB.id);
    const result = await getOrderAsUser(orderB.id, userA.id);
    expect(result).toBeNull();
  });

  it('userA cannot update userB posts', async () => {
    const postB = await createPost(userB.id);
    await expect(updatePostAsUser(postB.id, 'hacked', userA.id))
      .rejects.toThrow('Not found');

    const unchanged = await getPost(postB.id);
    expect(unchanged.content).not.toBe('hacked');
  });

  it('userA cannot delete userB resources', async () => {
    const resourceB = await createResource(userB.id);
    await expect(deleteResourceAsUser(resourceB.id, userA.id))
      .rejects.toThrow('Not found');
  });
});
```

- [ ] **Krok 2: Dodaj ten test jako bramkę CI/CD blokującą deploy przy niepowodzeniu**

---

## Zadanie 10: Redukcja danych — blokada masowego eksportu przez ORM

**Cel:** ORM (Drizzle/Prisma) domyślnie zwraca wszystkie kolumny, włącznie z hashami haseł i kluczami API.

- [ ] **Krok 1: Wymuś eksplicytne projekcje we wszystkich zapytaniach do DB**

```ts
// ŹLE — zwraca password_hash, api_key, internal_notes:
const user = await db.query.users.findFirst({ where: eq(users.id, id) });
return user;

// DOBRZE — tylko publiczne pola:
const user = await db.query.users.findFirst({
  where: eq(users.id, id),
  columns: { id: true, name: true, email: true, createdAt: true }
  // password, apiKey, resetToken — NIE
});
return user;
```

- [ ] **Krok 2: Dodaj linter/ESLint rule blokujący `findMany()` bez `columns` w kontekstach API**



## Zadanie 11: Walidacja schematu wejściowego Server Actions (Zod)

**Cel:** Server Actions są publicznymi endpointami HTTP — każdy może je wywołać z dowolnym payloadem.

- [ ] **Krok 1: Owiń każdą akcję serwerową walidacją Zod**

```ts
import { z } from 'zod';

const deletePostSchema = z.object({
  postId: z.string().uuid(),
}).strict(); // .strict() odrzuca nieznane pola

export async function deletePost(input: unknown) {
  const parsed = deletePostSchema.safeParse(input);
  if (!parsed.success) {
    throw new Error('Invalid request');
  }
  // ... logika
}
```

- [ ] **Krok 2: Test — Server Action odrzuca zniekształcony payload**

```ts
await expect(deletePost({ postId: 'not-a-uuid', extra: 'field' }))
  .rejects.toThrow('Invalid request');
```

---

## Zadanie 12: Wewnętrzny strażnik sesji w każdej akcji serwerowej

**Cel:** UI może ukrywać przyciski, ale atakujący wywołuje endpoint bezpośrednio. Każda mutacja musi samodzielnie weryfikować tożsamość.

- [ ] **Krok 1: Dodaj weryfikację sesji na początku każdej wrażliwej akcji**

```ts
'use server';

import { getSession } from '@/lib/session';

export async function deletePost(input: unknown) {
  // PIERWSZA linia — weryfikacja sesji, zanim cokolwiek innego
  const session = await getSession();
  if (!session?.user) {
    throw new Error('Unauthorized');
  }
  // ... walidacja, logika
}
```

---

## Zadanie 13: Weryfikacja własności rekordu (BOLA/IDOR)

**Cel:** Weryfikacja, że `session.user.id` = `resource.ownerId` przed każdą modyfikacją. Bez tego, zalogowany użytkownik może edytować zasoby innych użytkowników.

- [ ] **Krok 1: Dodaj weryfikację własności po pobraniu rekordu**

```ts
export async function updatePost(input: { postId: string; content: string }) {
  const session = await getSession();
  if (!session?.user) throw new Error('Unauthorized');

  const post = await db.posts.findUnique({ where: { id: input.postId } });
  if (!post) throw new Error('Not found');

  // Weryfikacja własności — BOLA/IDOR guard
  if (post.authorId !== session.user.id) {
    throw new Error('Forbidden');
  }

  return db.posts.update({ where: { id: input.postId }, data: { content: input.content } });
}
```

---

## Zadanie 14: Blokada wycieku danych ORM do klienta (Data Projection)

*→ Patrz Zadanie 10 dla strony bazodanowej. Tu chodzi o warstwę serwerową → klient.*

- [ ] **Krok 1: Mapuj explicite pola przed zwróceniem z Server Action/API**

```ts
const rawUser = await db.users.findUnique({ where: { id } });

// Zawsze zwracaj tylko pola przeznaczone dla klienta:
return {
  id: rawUser.id,
  name: rawUser.name,
  email: rawUser.email,
  // NIE zwracaj: rawUser.passwordHash, rawUser.apiKey, rawUser.stripeSecret
};
```

---

## Zadanie 15: React Taint API — zabezpieczenie przed wyciekiem obiektów do przeglądarki

**Cel:** Jeśli obiekt z bazy (np. z kluczem API) jest przekazywany przez component tree, może trafić do bundle klienta. React Taint API zatrzymuje build przy wykryciu takiego przekazania.

- [ ] **Krok 1: Oznacz wrażliwe obiekty jako zakazane do transferu sieciowego**

```ts
import { experimental_taintObjectReference } from 'react';

export async function getPrivateConfig() {
  const config = await db.configs.findUnique({ where: { id: 'main' } });
  
  // Oznaczenie — próba przekazania tego do Client Component przerwie build
  experimental_taintObjectReference(
    'Do not pass private config to the client!',
    config
  );
  
  return config; // Używaj tylko w Server Components
}
```

---

## Zadanie 16: Deklaracja `server-only` w modułach wrażliwych

**Cel:** Import modułu backendowego do Client Component powoduje wyciek sekretów do bundle JS.

- [ ] **Krok 1: Dodaj `import 'server-only'` do plików z logiką backendu**

```ts
// src/lib/crypto.ts
import 'server-only'; // Przerywa build jeśli plik zostanie zaimportowany po stronie klienta

export const decryptData = (encrypted: Buffer) => {
  return crypto.privateDecrypt(privateKey, encrypted);
};
```

---

## Zadanie 17: Ochrona CSRF dla Server Actions

**Cel:** Server Actions akceptują żądania POST z dowolnej domeny. Bez weryfikacji Origin, złośliwa witryna może wywołać akcję w imieniu zalogowanego użytkownika.

- [ ] **Krok 1: Weryfikuj nagłówek Origin w middleware lub na początku akcji**

```ts
import { headers } from 'next/headers';

export async function sensitiveAction(input: unknown) {
  const origin = headers().get('origin');
  const host = headers().get('host');

  if (!origin || !host || new URL(origin).host !== host) {
    throw new Error('Forbidden: invalid origin');
  }
  // ... logika
}
```

---

## Zadanie 18: Jawna kontrola CORS na trasach API

**Cel:** Wildcard `Access-Control-Allow-Origin: *` pozwala każdej domenie na dostęp do API. Użyj eksplicytnej białej listy.

- [ ] **Krok 1: Zdefiniuj i sprawdzaj listę dozwolonych origin**

```ts
const ALLOWED_ORIGINS = [
  'https://twoja-domena.pl',
  'https://app.twoja-domena.pl',
];

export async function GET(req: Request) {
  const origin = req.headers.get('origin');
  const res = new Response(JSON.stringify({ ok: true }));

  if (origin && ALLOWED_ORIGINS.includes(origin)) {
    res.headers.set('Access-Control-Allow-Origin', origin);
  }
  return res;
}
```

---

## Zadanie 19: Nagłówki bezpieczeństwa przeglądarki (Security Headers)

**Cel:** Bez nagłówków przeglądarka nie wie, że ma aktywować bariery ochronne (XSS, Clickjacking, MIME sniffing).

- [ ] **Krok 1: Skonfiguruj nagłówki w `next.config.ts`**

```ts
// next.config.ts
const securityHeaders = [
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Permissions-Policy', value: 'geolocation=(), microphone=(), camera=()' },
  {
    key: 'Content-Security-Policy',
    value: "default-src 'self'; script-src 'self' 'unsafe-inline'; frame-ancestors 'none';"
  },
];

module.exports = {
  headers: async () => [{
    source: '/(.*)',
    headers: securityHeaders,
  }],
};
```

---

## Zadanie 20: Ujednolicony klient akcji bezpiecznych (Safe Action Client)

**Cel:** Zamiast ręcznie dodawać `getSession()` do każdej akcji, wymuś autoryzację poprzez wzorzec dekoratora.

- [ ] **Krok 1: Stwórz `createSafeActionClient` z middleware autoryzacyjnym**

```ts
// src/lib/safe-action.ts
import { createSafeActionClient } from 'next-safe-action';
import { getSession } from './session';

export const authAction = createSafeActionClient({
  async middleware() {
    const session = await getSession();
    if (!session?.user) {
      throw new Error('Unauthenticated');
    }
    return { userId: session.user.id, role: session.user.role };
  },
});

// Użycie:
export const deletePost = authAction
  .schema(deletePostSchema)
  .action(async ({ parsedInput, ctx }) => {
    // ctx.userId i ctx.role zawsze dostępne i zweryfikowane
  });
```

---

## Zadanie 21: Wieloflagowe ciasteczka sesyjne (HttpOnly, Secure, SameSite)

**Cel:** JWT w `localStorage` = kradnij mnie. Token w `httpOnly` cookie nie jest dostępny dla żadnego skryptu JavaScript.

- [ ] **Krok 1: Ustaw tokeny sesyjne w ciasteczkach z twardymi flagami**

```ts
import { cookies } from 'next/headers';

export async function setSessionCookie(token: string) {
  cookies().set('session', token, {
    httpOnly: true,       // niewidoczne dla JS
    secure: true,         // tylko HTTPS
    sameSite: 'strict',   // brak wysyłania przy cross-site requests
    path: '/',
    maxAge: 60 * 60 * 24, // 24 godziny
  });
}
```

---

## Zadanie 22: Czyszczenie LocalStorage z pozostałości sesyjnych

**Cel:** Użytkownicy, którzy logowali się przed migracją na ciasteczka, mogą mieć stare tokeny w `localStorage`.

- [ ] **Krok 1: Dodaj oczyszczanie pamięci przeglądarki przy starcie aplikacji**

```ts
// src/app/layout.tsx lub src/components/SessionCleaner.tsx
'use client';
import { useEffect } from 'react';

export function SessionCleaner() {
  useEffect(() => {
    // Usuń stare tokeny sesyjne z jawnej pamięci przeglądarki
    localStorage.removeItem('supabase.auth.token');
    localStorage.removeItem('sb-access-token');
    localStorage.removeItem('sb-refresh-token');
    sessionStorage.clear();
  }, []);
  return null;
}
```

---

## Zadanie 23: Weryfikacja algorytmu podpisu JWT

**Cel:** Atakujący może podmienić nagłówek JWT na `"alg": "none"` lub zmienić algorytm z RS256 na HS256 z kluczem publicznym jako sekretem.

- [ ] **Krok 1: Jawnie deklaruj dozwolone algorytmy przy weryfikacji JWT**

```ts
import jwt from 'jsonwebtoken';

// ŹLE — akceptuje dowolny algorytm, w tym 'none':
const payload = jwt.verify(token, publicKey);

// DOBRZE — tylko zaufane algorytmy:
const payload = jwt.verify(token, publicKey, {
  algorithms: ['RS256', 'ES256'], // nigdy nie dodawaj 'none' ani 'HS256' przy kluczach asymetrycznych
});
```

---

## Zadanie 24: Dynamiczne pobieranie i rotacja kluczy JWKS

**Cel:** Zakodowany na stałe klucz publiczny nie może być rotowany bez redeploymentu. JWKS URI pozwala na bezpieczną rotację bez zmian kodu.

- [ ] **Krok 1: Pobieraj klucze weryfikacji z JWKS URI dostawcy auth**

```ts
import jwksClient from 'jwks-rsa';

const client = jwksClient({
  jwksUri: 'https://twoja-domena.pl/.well-known/jwks.json',
  cache: true,
  cacheMaxEntries: 5,
  cacheMaxAge: 600_000, // 10 minut
});

function getKey(header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) {
  client.getSigningKey(header.kid, (err, key) => {
    callback(err, key?.getPublicKey());
  });
}
```

---

## Zadanie 25: Dynamiczna rotacja identyfikatorów sesji

**Cel:** Stały identyfikator sesji pozwala na atak "Session Fixation". Nowy token po uwierzytelnieniu eliminuje ten wektor.

- [ ] **Krok 1: Generuj nowy token sesji przy każdym logowaniu i zmianie uprawnień**

```ts
export async function login(credentials: Credentials) {
  const user = await verifyCredentials(credentials);
  
  // Unieważnij starą sesję
  await session.revoke(request.cookies.get('session')?.value);
  
  // Wygeneruj nową
  const newToken = await session.create({ userId: user.id });
  cookies().set('session', newToken, { httpOnly: true, secure: true });
}
```

---

## Zadanie 26: Krótki czas życia sesji administracyjnych

**Cel:** Sesja admina nierotowana przez 8 godzin to poważne zagrożenie przy przejęciu urządzenia.

- [ ] **Krok 1: Ustaw agresywny maxAge dla ciasteczek sesji admina**

```ts
if (user.role === 'admin') {
  cookies().set('admin_session', adminToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 15, // 15 minut bezczynności — koniec sesji
  });
}
```

---

## Zadanie 27: Programistyczne bramkowanie MFA

**Cel:** Wywołanie akcji wysokiego ryzyka (płatność, zmiana hasła, eksport danych) wymaga potwierdzenia drugiego składnika bezpośrednio w kodzie.

- [ ] **Krok 1: Dodaj bramkę MFA w kodzie akcji przed wykonaniem operacji**

```ts
export async function initiatePayment(input: PaymentInput) {
  const session = await getSession();
  if (!session?.user) throw new Error('Unauthorized');

  if (!session.user.mfaVerified) {
    return {
      status: 'MFA_REQUIRED',
      challengeId: await generateMfaChallenge(session.user.id),
    };
  }
  // Dopiero teraz: logika płatności
}
```

---

## Zadanie 28: Blokada brute-force logowania

**Cel:** Bot może próbować tysięcy kombinacji hasła w ciągu sekund. Limiter licznika udaremnia to bez wpływu na prawdziwych użytkowników.

- [ ] **Krok 1: Implementuj licznik nieudanych prób logowania w Redis/KV**

```ts
import { kv } from '@vercel/kv'; // lub Cloudflare KV / Redis

export async function login(email: string, password: string) {
  const key = `login_fails:${email}`;
  const attempts = Number(await kv.get(key) ?? 0);

  if (attempts >= 5) {
    throw new Error('Account temporarily locked. Try again in 15 minutes.');
  }

  const valid = await verifyPassword(email, password);

  if (!valid) {
    await kv.set(key, attempts + 1, { ex: 900 }); // 15 minut blokady
    throw new Error('Invalid credentials');
  }

  await kv.del(key); // Reset po udanym logowaniu
}
```

---

## Zadanie 29: Silne haszowanie haseł (bcrypt z wysokim kosztem)

**Cel:** Przy wycieku bazy danych, słabo zahaszowane hasła (MD5, SHA1, bcrypt cost=6) są łamalne w godzinach.

- [ ] **Krok 1: Ustaw wysoki współczynnik kosztu bcrypt**

```ts
import bcrypt from 'bcrypt';

const BCRYPT_COST = 12; // Minimum 12 — benchmark w środowisku docelowym

export async function hashPassword(raw: string): Promise<string> {
  return bcrypt.hash(raw, BCRYPT_COST);
}

export async function verifyPassword(raw: string, hash: string): Promise<boolean> {
  return bcrypt.compare(raw, hash);
}
```

- [ ] **Krok 2: Uruchom benchmark kosztu w środowisku produkcyjnym**

```ts
// Zmierz p95 czasu haszowania — musi być < 300ms dla UX logowania
const start = Date.now();
await bcrypt.hash('test', BCRYPT_COST);
console.log(`bcrypt cost=${BCRYPT_COST}: ${Date.now() - start}ms`);
```

---

## Zadanie 30: Bezpieczne, deklaratywne ładowanie zmiennych środowiskowych

**Cel:** Aplikacja uruchomiona bez krytycznych kluczy działa w niebezpiecznym stanie domyślnym (pusty secret, null URL bazy).

- [ ] **Krok 1: Stwórz typowany schemat środowiskowy — Fail-Closed przy starcie**

```ts
// src/lib/env.ts
import { z } from 'zod';

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  NEXT_PUBLIC_APP_URL: z.string().url(),
  NODE_ENV: z.enum(['development', 'production', 'test']),
});

// Parsowanie przy starcie — rzuca wyjątkiem gdy brak wymaganego klucza
export const env = envSchema.parse(process.env);
```

- [ ] **Krok 2: Zaimportuj `env` tylko z tego modułu (nie bezpośrednio `process.env`)**

---

## Zadanie 31: Sliding Window Rate Limiter na kluczowych endpointach

**Cel:** Bez limitera, jeden IP może zalać login/register/API tysiącami żądań na sekundę.

- [ ] **Krok 1: Implementuj rate limiter oparty o Sliding Window w Redis/KV**

```ts
import { Ratelimit } from '@upstash/ratelimit';
import { kv } from '@vercel/kv';

const ratelimit = new Ratelimit({
  redis: kv,
  limiter: Ratelimit.slidingWindow(10, '60 s'), // 10 req/minutę
});

export async function POST(req: Request) {
  const ip = req.headers.get('x-real-ip') ?? '127.0.0.1';
  const { success, remaining } = await ratelimit.limit(`rate:${ip}`);

  if (!success) {
    return new Response('Too Many Requests', {
      status: 429,
      headers: { 'Retry-After': '60' },
    });
  }
  // ... logika endpointu
}
```

---

## Zadanie 32: Zaufane rozwiązywanie adresu IP klienta

**Cel:** Nagłówek `X-Forwarded-For` może być sfabrykowany przez klienta, co pozwala na ominięcie rate limitera.

- [ ] **Krok 1: Pobieraj IP wyłącznie z nagłówków zaufanego proxy**

```ts
// Za Cloudflare — użyj CF-Connecting-IP (niefałszowalny):
const ip = req.headers.get('cf-connecting-ip');

// Za Vercel — użyj x-real-ip (wstrzykiwany przez Vercel):
const ip = req.headers.get('x-real-ip');

// NIGDY nie ufaj:
// req.headers.get('x-forwarded-for') — może być sfabrykowany przez klienta
```

---

## Zadanie 33: Wymuszenie weryfikacji urządzenia mobilnego (App Attestation)

**Cel:** Bez attestation, każdy skrypt może podszywać się pod aplikację mobilną i wywołać API mobilne.

- [ ] **Krok 1: Weryfikuj token attestacji przy wrażliwych operacjach API mobilnego**

```ts
export async function mobileAction(req: Request) {
  const attestToken = req.headers.get('x-app-attest-token');

  if (!attestToken) {
    return new Response('Untrusted device', { status: 403 });
  }

  const result = await verifyAppAttest(attestToken);
  if (!result.valid) {
    return new Response('Attestation failed', { status: 403 });
  }
  // ... logika
}
```

---

## Zadanie 34: Rygorystyczny limit rozmiaru payloadu JSON

**Cel:** Bez limitu, jeden request z 100MB body zablokuje Node.js Event Loop na sekundy.

- [ ] **Krok 1: Ustaw globalny limit rozmiaru body żądania**

```ts
// Express / Node.js:
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ limit: '100kb', extended: true }));

// Next.js App Router — w route handler:
export async function POST(req: Request) {
  const contentLength = req.headers.get('content-length');
  if (contentLength && parseInt(contentLength) > 100 * 1024) {
    return new Response('Payload Too Large', { status: 413 });
  }
  // ... parsowanie
}
```

---

## Zadanie 35: Nieblokująca pętla zdarzeń Node.js (Worker Threads)

**Cel:** Ciężkie operacje (szyfrowanie, parsowanie PDF, kompresja) blokują Event Loop, paraliżując serwer dla wszystkich żądań.

- [ ] **Krok 1: Wynieś ciężkie operacje do Worker Threads**

```ts
// worker/encrypt.ts
import { parentPort, workerData } from 'worker_threads';
import { encrypt } from '../lib/crypto';

parentPort?.postMessage(encrypt(workerData));

// Wywołanie z głównego wątku:
function runWorker(data: Buffer): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker/encrypt.js', { workerData: data });
    worker.on('message', resolve);
    worker.on('error', reject);
  });
}
```

---

## Zadanie 36: Zarządzanie timeoutami gniazd (Slowloris DoS)

**Cel:** Atak Slowloris otwiera wiele połączeń HTTP, wysyłając nagłówki bardzo powoli. Serwer trzyma połączenia otwarte aż do wyczerpania zasobów.

- [ ] **Krok 1: Ustaw twarde timeouty na gniezdzie serwera Node.js**

```ts
import { createServer } from 'http';

const server = createServer(app);

server.headersTimeout = 60_000;   // 60s na wysłanie nagłówków
server.requestTimeout = 30_000;   // 30s na kompletne żądanie
server.keepAliveTimeout = 5_000;  // 5s na nowe żądanie w tej samej sesji TCP
```

---

## Zadanie 37: Weryfikacja sygnatury webhooka przed parsowaniem JSON

**Cel:** W Next.js App Router strumień żądania jest jednorazowy. Odczytanie body do JSON niszczy możliwość weryfikacji sygnatury. Surowy tekst musi być pobierany PRZED konwersją na JSON.

- [ ] **Krok 1: Weryfikuj sygnaturę Stripe z surowego `request.text()`**

```ts
// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';

export async function POST(req: Request) {
  // 1. Najpierw surowy tekst — sygnatura liczy się od bajtów, nie od JSON
  const rawBody = await req.text();
  const sig = req.headers.get('stripe-signature') ?? '';

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch (err) {
    return new Response('Webhook signature verification failed', { status: 400 });
  }

  // 2. Dopiero teraz przetwarzaj zdarzenie
  switch (event.type) {
    case 'payment_intent.succeeded': break;
    // ...
  }
  return new Response('ok');
}
```

---

## Zadanie 38: Weryfikacja okna czasowego webhooka (Replay Attack)

**Cel:** Atakujący może przechwycić prawidłowo podpisany webhook i wysłać go ponownie po tygodniu (Replay Attack).

- [ ] **Krok 1: Biblioteka Stripe weryfikuje timestamp automatycznie — sprawdź konfigurację**

```ts
// constructEvent() domyślnie odrzuca webhooki starsze niż 5 minut
// Aby to potwierdzić, nie ustawiaj tolerancji na więcej niż 300 sekund:
event = stripe.webhooks.constructEvent(rawBody, sig, secret, 300); // max 5 min
```

- [ ] **Krok 2: Dla niestandardowych webhooków — implementuj własną weryfikację timestamp**

```ts
const timestamp = parseInt(req.headers.get('x-webhook-timestamp') ?? '0');
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) {
  return new Response('Webhook too old — possible replay attack', { status: 400 });
}
```

---

## Zadanie 39: Biała lista Open Redirect

**Cel:** Parametr `?redirect=/evil.com` po logowaniu może przekierować użytkownika na stronę phishingową.

- [ ] **Krok 1: Weryfikuj cel przekierowania przed wykonaniem**

```ts
export function safeRedirect(redirectParam: string | null, defaultPath = '/dashboard') {
  if (!redirectParam) return defaultPath;

  try {
    const target = new URL(redirectParam, 'https://twoja-domena.pl');
    // Zezwól tylko na redirect w obrębie własnej domeny
    if (target.origin !== 'https://twoja-domena.pl') {
      console.warn('Blocked open redirect to:', target.origin);
      return defaultPath;
    }
    return target.pathname + target.search;
  } catch {
    return defaultPath;
  }
}
```

---

## Zadanie 40: Ograniczenie głębokości zapytań GraphQL

**Cel:** Głęboko zagnieżdżone zapytania GraphQL (`user { posts { comments { author { posts { ... } } } } }`) mogą wyczerpać CPU i pamięć serwera.

- [ ] **Krok 1: Zainstaluj i skonfiguruj limit głębokości zapytań**

```ts
import depthLimit from 'graphql-depth-limit';
import { graphqlHTTP } from 'express-graphql';

app.use('/graphql', graphqlHTTP({
  schema,
  validationRules: [
    depthLimit(4), // maksymalna głębokość zagnieżdżenia = 4
  ],
}));
```

---

## Zadanie 41: Typowane schematy środowiskowe dla Cloudflare Workers

**Cel:** Worker uruchomiony bez wymaganego binding zwraca niezrozumiałe błędy runtime. Fail-closed = zablokuj start.

- [ ] **Krok 1: Stwórz schemat Zod dla środowiska Worker**

```ts
// apps/edge-api/src/env.ts
import { z } from 'zod';

const edgeApiEnvSchema = z.object({
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  CACHE_KV: z.any(), // Cloudflare KV namespace
  ASSETS_BUCKET: z.any(), // R2 Bucket binding
  ENVIRONMENT: z.enum(['production', 'staging']),
});

export function parseEdgeApiEnv(input: unknown) {
  const result = edgeApiEnvSchema.safeParse(input);
  if (!result.success) {
    // Zwróć 503 configuration_error — nie wyciągnij szczegółów Zod na zewnątrz
    throw new Response('Service Unavailable', { status: 503 });
  }
  return result.data;
}
```

---

## Zadanie 42: Skaner wycieków artefaktów publicznych (CI/CD Gate)

**Cel:** Source mapy, wewnętrzne hosty, klucze API w bundle JS to informacje, które atakujący mogą wykorzystać do planowania ataku.

- [ ] **Krok 1: Stwórz skrypt skanujący artefakty po buildzie**

```js
// scripts/security/assert-public-artifacts.mjs
import fs from 'node:fs';
import path from 'node:path';
import assert from 'node:assert/strict';

const BUILD_DIR = './dist';
const DENY_LIST = [
  'DATABASE_URL', 'JWT_SECRET', 'STRIPE_SECRET',
  'auth-worker.internal', 'core-service.internal',
];

function scanDir(dir) {
  for (const file of fs.readdirSync(dir, { recursive: true })) {
    const fullPath = path.join(dir, file);
    if (fs.statSync(fullPath).isFile()) {
      // Source mapy
      assert.ok(!file.endsWith('.map'), `Source map found in production build: ${file}`);
      // Sekrety
      const content = fs.readFileSync(fullPath, 'utf8');
      for (const term of DENY_LIST) {
        assert.ok(!content.includes(term), `Secret "${term}" found in public artifact: ${file}`);
      }
    }
  }
}

scanDir(BUILD_DIR);
console.log('✅ Public artifact scan passed.');
```

- [ ] **Krok 2: Dodaj do `package.json` i CI**

```json
"security:artifacts": "npm run build && node scripts/security/assert-public-artifacts.mjs"
```

---

## Zadanie 43: Blokada ekspozycji kodu serwerowego w konsoli przeglądarki

**Cel:** `console.log(session)`, `console.error(err.stack)` w kodzie produkcyjnym wycieka informacje o architekturze systemu.

- [ ] **Krok 1: Stwórz skrypt skanujący kod źródłowy przed buildem**

```js
// scripts/security/assert-browser-source.mjs
import { execSync } from 'node:child_process';

const violations = execSync(
  'grep -rn "console\\.log\\|console\\.error.*Error\\|console\\.warn" src/app src/components --include="*.tsx" --include="*.ts"',
  { encoding: 'utf8', stdio: 'pipe' }
).trim();

if (violations) {
  console.error('❌ Production console statements found:\n', violations);
  process.exit(1);
}
console.log('✅ Browser source check passed.');
```

---

## Zadanie 44: Centralna polityka klasyfikacji tras (Route Security Policy)

**Cel:** Każdy publicznie dostępny endpoint musi mieć jawną klasyfikację: auth, authz, inputSchema, rateLimitClass.

- [ ] **Krok 1: Stwórz `docs/security/production-route-security-policy.json`**

```json
{
  "routes": [
    {
      "surface": "edge-api",
      "method": "POST",
      "path": "/api/auth/login",
      "auth": "none",
      "authz": "public",
      "inputSchema": "LoginSchema",
      "bodyLimitClass": "auth-json",
      "rateLimitClass": "strict",
      "owner": "auth-team",
      "evidence": "test: auth-worker.test.ts"
    }
  ]
}
```

- [ ] **Krok 2: Stwórz skrypt weryfikujący, że żaden handler nie istnieje bez wpisu w policy**

---

## Zadanie 45: Ochrona przed spoofingiem nagłówków wewnętrznych w klastrze

**Cel:** Jeśli Worker A przekazuje nagłówki od klienta do Workera B, atakujący może wstrzyknąć sfabrykowane `x-auth-user-id`.

- [ ] **Krok 1: Odbuduj nagłówki autoryzacyjne z zaufanego ciasteczka, nie z przychodzącego żądania**

```ts
// apps/edge-api/src/routes/core.ts
export async function proxyToCoreService(req: Request, env: EdgeApiEnv) {
  const session = await verifySessionCookie(req, env);

  // Przekaż tylko nagłówki odbudowane z weryfikowanej sesji
  const internalHeaders = new Headers({
    'content-type': req.headers.get('content-type') ?? 'application/json',
    'x-auth-user-id': session.userId,       // z zaufanej sesji
    'x-auth-user-role': session.role,        // z zaufanej sesji
    'x-correlation-id': crypto.randomUUID(), // nowe — nie z klienta
    // NIGDY nie przekazuj: x-forwarded-for, authorization z klienta
  });

  return env.CORE_SERVICE.fetch(new Request(coreServiceUrl, {
    method: req.method,
    headers: internalHeaders,
    body: req.body,
  }));
}
```

---

## Zadanie 46: Kontrola buforowania CDN (zakaz cache'owania odpowiedzi autoryzowanych)

**Cel:** Odpowiedź dla zalogowanego użytkownika trafiająca do cache CDN = wyciek danych do innych użytkowników.

- [ ] **Krok 1: Ustaw jawne nagłówki Cache-Control dla odpowiedzi autoryzowanych**

```ts
// Dla endpointów wymagających autoryzacji:
return new Response(JSON.stringify(data), {
  headers: {
    'Cache-Control': 'private, no-store, no-cache, must-revalidate',
    'Content-Type': 'application/json',
  },
});

// Dla publicznych zasobów statycznych (można cachować):
return new Response(data, {
  headers: {
    'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
  },
});
```

---

## Zadanie 47: Timeout asynchronicznych żądań wewnętrznych (withDeadline)

**Cel:** Stająca wewnętrzna usługa (DB, zewnętrzne API) blokuje Worker na czas domyślnego timeoutu platformy (30s+), zamrażając zasoby.

- [ ] **Krok 1: Stwórz helper `withDeadline` i używaj go we wszystkich wywołaniach**

```ts
// packages/cloudflare-runtime/src/request-security.ts
export async function withDeadline<T>(
  promise: Promise<T>,
  timeoutMs: number,
  label: string
): Promise<T> {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error(`Deadline exceeded: ${label}`)), timeoutMs)
  );
  return Promise.race([promise, timeout]);
}

// Użycie:
const data = await withDeadline(
  env.CORE_SERVICE.fetch(req),
  5_000, // 5 sekund maksymalnie
  'core-service fetch'
).catch(() => {
  return new Response('Service Unavailable', { status: 503 });
});
```

---

## Zadanie 48: Security Control Matrix — bramka CI/CD dla deploymentu

**Cel:** Każda z 40 kontroli z tej checklisty musi być udokumentowana w macierzy bezpieczeństwa, a skrypt weryfikujący jej kompletność musi przechodzić przed deployem.

- [ ] **Krok 1: Stwórz `docs/security/application-security-control-matrix.md`**

```markdown
| # | Kontrola | Status | Owner | Evidence |
|---|----------|--------|-------|----------|
| 1 | RLS aktywne | implemented | backend | migration: 001_enable_rls.sql |
| 2 | InitPlan RLS | implemented | backend | migration: 002_optimize_rls_initplan.sql |
...
```

- [ ] **Krok 2: Skrypt sprawdzający kompletność macierzy**

```bash
npm run security:matrix  # Musi przejść przed każdym deployem do produkcji
```

---

## Zadanie 49: Zero Trust dla panelu admina (Cloudflare Access)

**Cel:** Subdomeny stagingowe i panel admina dostępne dla całego internetu to krytyczne zagrożenie.

- [ ] **Krok 1: Dodaj Cloudflare Access jako bramkę Zero Trust**

```hcl
# infra/cloudflare/terraform/access.tf
resource "cloudflare_access_application" "admin_panel" {
  zone_id          = var.cloudflare_zone_id
  name             = "Admin Panel"
  domain           = "admin.twoja-domena.pl"
  session_duration = "1h"
  
  # Tylko whitelistowane e-maile lub IP organizacji
}

resource "cloudflare_access_policy" "admin_email_policy" {
  application_id = cloudflare_access_application.admin_panel.id
  precedence     = 1
  decision       = "allow"
  
  include {
    email = ["admin@twoja-firma.pl"]
  }
}
```

---

## Zadanie 50: Reguły WAF Cloudflare Free — kompletna konfiguracja

**Cel:** 5 reguł Custom WAF w darmowym planie Cloudflare blokuje całe klasy ataków zanim dotrą do aplikacji.

- [ ] **Krok 1: Stwórz `infra/cloudflare/terraform/waf_free.tf`**

```hcl
resource "cloudflare_ruleset" "zone_custom_firewall" {
  zone_id = var.cloudflare_zone_id
  name    = "Custom Free WAF Rules"
  kind    = "zone"
  phase   = "http_request_firewall_custom"

  rules = [
    {
      ref         = "protocol_method_guard"
      description = "Blokuj niestandardowe porty i nieoczekiwane metody HTTP"
      expression  = "(not cf.edge.server_port in {80 443}) or (not http.request.method in {\"GET\" \"HEAD\" \"POST\" \"OPTIONS\"})"
      action      = "block"
      enabled     = true
    },
    {
      ref         = "auth_abuse_guard"
      description = "Managed Challenge dla endpointów auth wysokiego ryzyka"
      expression  = "(http.request.method ne \"OPTIONS\") and (starts_with(http.request.uri.path, \"/api/auth/\") or starts_with(http.request.uri.path, \"/api/login\") or starts_with(http.request.uri.path, \"/api/register\"))"
      action      = "managed_challenge"
      enabled     = true
    },
    {
      ref         = "scanner_garbage_guard"
      description = "Blokuj skanery i ścieżki popularne w atakach"
      expression  = "(starts_with(http.request.uri.path, \"/wp-\") or starts_with(http.request.uri.path, \"/phpmyadmin\") or starts_with(http.request.uri.path, \"/.git\") or starts_with(http.request.uri.path, \"/.env\") or starts_with(http.request.uri.path, \"/backup\"))"
      action      = "block"
      enabled     = true
    },
    {
      ref         = "admin_preview_guard"
      description = "Blokuj publiczny dostęp do admin i subdomen preview/stage"
      expression  = "(http.request.method ne \"OPTIONS\") and (starts_with(http.request.uri.path, \"/api/admin/\") or starts_with(http.host, \"preview.\") or starts_with(http.host, \"stage.\"))"
      action      = "block"
      enabled     = true
    },
    {
      ref         = "emergency_cost_shield"
      description = "Awaryjna tarcza API — aktywuj ręcznie przy incydencie DDoS"
      expression  = "(starts_with(http.request.uri.path, \"/api/\") and http.request.method ne \"OPTIONS\")"
      action      = "managed_challenge"
      enabled     = false
    },
  ]
}
```

- [ ] **Krok 2: Weryfikacja wyrażeń WAF (Free plan nie obsługuje regex)**

```bash
node scripts/security/cloudflare-waf-expressions.test.mjs
# Oczekiwane: ✅ Cloudflare Free WAF expression checks passed.
```

- [ ] **Krok 3: Commit końcowy**

```bash
git add infra/cloudflare/terraform/waf_free.tf scripts/security/
git commit -m "security: complete WAF and security hardening implementation"
```

---

*Plik wygenerowany: 2026-06-15 | Wersja: 2.0 | Przeznaczenie: implementacja przez agenta AI*
*Obejmuje: 40 mechanizmów z literatury Vibe Coding Security + 10 hardeningów infrastruktury Cloudflare*
