Files
recipe-app/_archive/docs/Säkerhetshärdningsplan för Recipe-app.md
Nils-Johan Gynther ca8987d9e4
Test Suite / test (24.15.0) (push) Has been cancelled
Add comprehensive documentation for Flutter frontend migration and backend review
- Introduced user guide for Flutter frontend in README.md, detailing user flows and recent improvements.
- Created next steps roadmap for Flutter migration in next_steps_flutter.md, outlining current tasks and priorities.
- Developed technical description for Flutter frontend in teknisk_beskrivning_flutter.md, covering architecture and security status.
- Removed outdated migration documentation for Prisma P3009 and added recovery steps for failed migrations in migrering-MSI.md.
- Established a release checklist for product launches in produktlansering.md, ensuring security and stability measures are met.
- Formulated a systematic backend review and optimization plan in review_backend.md, focusing on reducing complexity and improving performance.
2026-05-10 00:28:59 +02:00

415 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔒 Säkerhetshärdningsplan för Recipe-App (Flutter + NestJS + MariaDB)
# Nyheter och förbättringar (2026-05-10)
- **Admin-inventarie:** Endast admin har tillgång till CRUD, merge, filter, sortering och preview för alla användares inventarieposter. Endpoints och UI är skyddade med @Roles('admin') och testade.
- **User-scope och IDOR-skydd:** Inventory och produkter är strikt user-scopade. Alla operationer kräver och filtrerar på userId. Tester verifierar att åtkomst nekas vid försök till IDOR.
- **Säkerhetshärdning:** DTO-validering, guard-ordning, logging, throttling, merge abuse-skydd, och rollbaserad access är implementerat och testat.
- **Optimeringar:** DRY i service-lager, striktare query parsing, preview-cache, API-cleanup, och kodduplication eliminerad.
- **Testtäckning:** Utökade enhets-, integrations- och säkerhetstester för alla kritiska flöden.
**Reviderad:** 2026-05-07 — baserad på faktisk kodgranskning av repo.
**Mål:** Täppa till IDOR, Full Table Dump, och andra kritiska säkerhetshål i **backend (NestJS/Prisma/MariaDB)**, **Flutter-frontend**, och **infrastruktur (Docker/Gitea/Ubuntu)**.
**Prioritet:** CRITICAL → HIGH → MEDIUM
---
## ✅ **Redan implementerat — kräver ingen åtgärd**
Följande är bekräftat implementerat i koden och behöver inte åtgärdas:
| Åtgärd | Var |
|---|---|
| Helmet med HSTS, X-Frame-Options, CORP, COOP, Referrer-Policy | `backend/src/main.ts` |
| CORS begränsat till `ALLOWED_ORIGIN` (env-var) | `backend/src/main.ts` |
| Global `JwtAuthGuard` (alla endpoints kräver JWT om inte `@Public()`) | `backend/src/app.module.ts` |
| Global `RolesGuard` med `@Roles('admin')` decorator | `backend/src/app.module.ts` |
| Global `ThrottlerGuard` (120 anrop/min), 10/min på login/register | `backend/src/app.module.ts`, `auth.controller.ts` |
| `ValidationPipe` med `whitelist` + `forbidNonWhitelisted` (förhindrar mass assignment) | `backend/src/main.ts` |
| bcrypt salt 12 för lösenordshashning | `backend/src/auth/auth.service.ts` |
| IDOR-skydd: `recipes`, `pantry`, `meal-plan`, `receipt-alias` filtrerar på `userId` | Respektive controllers |
| Produkter är user-scoped (`ownerId``Product`) | `backend/prisma/schema.prisma` |
| Hemligheter som env-vars i compose, inga hårdkodade värden | `compose.yml` |
| `.env.*` i `.gitignore` | `.gitignore` |
| Token-lagringsabstraktion i Flutter (`ITokenStorage`) | `flutter/lib/core/platform/token_storage.dart` |
| Premium/admin-guard (`PremiumOrAdminGuard`) | `backend/src/auth/premium-or-admin.guard.ts` |
---
---
## 📌 **0. Förberedelser**
### 0.1. Miljö och verktyg
- **Skapa en `security-audit`-gren:**
```bash
git checkout -b security-audit-$(date +%Y%m%d)
```
---
---
## 🚨 **1. IDOR + Full Table Dump i Inventory — CRITICAL**
**Problem:** `InventoryItem` saknar `userId`-fält i Prisma-schemat och `inventory.controller.ts` använder inte `@CurrentUser()` alls. Alla autentiserade användare kan läsa, modifiera och radera varandras inventarieposter via `GET/POST/PATCH/DELETE /api/inventory`.
**Nuläge i koden:**
- `InventoryItem`-modellen i `schema.prisma` saknar `userId`-kolumn
- `inventory.controller.ts` har noll anrop till `@CurrentUser()`
- `inventory.service.ts` metoder `findAll()`, `findExpiring()`, `update()`, `remove()` tar inte in `userId`
**Lösning — 3 steg:**
### Steg 1: Lägg till `userId` i Prisma-schemat
```prisma
// backend/prisma/schema.prisma
model InventoryItem {
id Int @id @default(autoincrement())
userId Int // NY KOLUMN
productId Int
// ...övriga fält oförändrade...
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
consumptions InventoryConsumption[]
@@index([productId])
@@index([userId]) // NY INDEX
}
// Lägg till i User-modellen:
model User {
// ...
inventoryItems InventoryItem[] // NY RELATION
}
```
Skapa migrerings-SQL i `backend/prisma/migrations/{timestamp}_add_userid_to_inventory/migration.sql`:
```sql
ALTER TABLE `InventoryItem` ADD COLUMN `userId` INT NOT NULL DEFAULT 1;
-- Backfill: sätt userId baserat på produktens ägare
UPDATE `InventoryItem` ii
JOIN `Product` p ON ii.productId = p.id
SET ii.userId = p.ownerId;
ALTER TABLE `InventoryItem` ADD CONSTRAINT `fk_inventory_user`
FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE;
CREATE INDEX `InventoryItem_userId_idx` ON `InventoryItem`(`userId`);
```
> **OBS:** Granska backfill-SQL mot din data innan körning — om produkter saknar ägare kan DEFAULT 1 ge fel.
### Steg 2: Uppdatera service-metoderna
```typescript
// backend/src/inventory/inventory.service.ts
// findAll — lägg till userId-parameter och where-klausul
async findAll(userId: number, query?: InventoryQuery) {
const where: Prisma.InventoryItemWhereInput = { userId };
// ...resten oförändrat...
}
// findExpiring — filtrera på userId
async findExpiring(userId: number) {
return this.prisma.inventoryItem.findMany({
where: { userId, bestBeforeDate: { not: null, gte: new Date() } },
// ...
});
}
// create — spara userId
async create(userId: number, data: CreateInventoryDto) {
return this.prisma.inventoryItem.create({
data: { ...data, userId, quantity: new Prisma.Decimal(data.quantity) },
// ...
});
}
// update — verifiera ägarskap innan uppdatering
async update(id: number, userId: number, data: UpdateInventoryDto) {
const existing = await this.findInventoryItemByIdOrThrow(id);
if (existing.userId !== userId) throw new ForbiddenException('Åtkomst nekad');
// ...resten oförändrat...
}
// remove — verifiera ägarskap innan borttagning
async remove(id: number, userId: number) {
const existing = await this.findInventoryItemByIdOrThrow(id);
if (existing.userId !== userId) throw new ForbiddenException('Åtkomst nekad');
return this.prisma.inventoryItem.delete({ where: { id } });
}
// consume — verifiera ägarskap
async consume(id: number, userId: number, body: ConsumeInventoryDto) {
const existing = await this.findInventoryItemByIdOrThrow(id);
if (existing.userId !== userId) throw new ForbiddenException('Åtkomst nekad');
// ...resten oförändrat...
}
```
### Steg 3: Uppdatera controller
```typescript
// backend/src/inventory/inventory.controller.ts
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { ForbiddenException } from '@nestjs/common';
@Controller('inventory')
export class InventoryController {
@Get()
findAll(
@CurrentUser() user: { userId: number },
@Query('location') location?: string,
@Query('sort') sort?: string,
) {
return this.inventoryService.findAll(user.userId, { location, sort });
}
@Get('expiring')
findExpiring(@CurrentUser() user: { userId: number }) {
return this.inventoryService.findExpiring(user.userId);
}
@Post()
create(
@CurrentUser() user: { userId: number },
@Body() body: CreateInventoryDto,
) {
return this.inventoryService.create(user.userId, body);
}
@Patch(':id')
update(
@CurrentUser() user: { userId: number },
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdateInventoryDto,
) {
return this.inventoryService.update(id, user.userId, body);
}
@Delete(':id')
remove(
@CurrentUser() user: { userId: number },
@Param('id', ParseIntPipe) id: number,
) {
return this.inventoryService.remove(id, user.userId);
}
@Post(':id/consume')
consume(
@CurrentUser() user: { userId: number },
@Param('id', ParseIntPipe) id: number,
@Body() body: ConsumeInventoryDto,
) {
return this.inventoryService.consume(id, user.userId, body);
}
@Get(':id/consumption-history')
findConsumptionHistory(
@CurrentUser() user: { userId: number },
@Param('id', ParseIntPipe) id: number,
) {
// Verifiera ägarskap på item innan historik returneras
return this.inventoryService.findConsumptionHistory(id, user.userId);
}
}
```
### Steg 4: Testa
```typescript
// backend/src/inventory/inventory.service.spec.ts
describe('IDOR-skydd inventory', () => {
it('nekar åtkomst till annan användares post vid update', async () => {
prismaMock.inventoryItem.findUnique.mockResolvedValue({ id: 1, userId: 2 } as any);
await expect(service.update(1, 1, {})).rejects.toThrow(ForbiddenException);
});
it('nekar åtkomst till annan användares post vid remove', async () => {
prismaMock.inventoryItem.findUnique.mockResolvedValue({ id: 1, userId: 2 } as any);
await expect(service.remove(1, 1)).rejects.toThrow(ForbiddenException);
});
});
```
---
---
## **2. dist/ saknas i .gitignore — HIGH**
**Problem:** `backend/dist/` och `backend/tsconfig.tsbuildinfo` är committade i repot och orsakar merge-konflikter vid deploy (bekräftat 2026-05-07).
**Lösning:**
Lägg till i `.gitignore` (rotnivå):
```gitignore
# Kompilerat backend-output — byggs i Docker, ska ej spåras
backend/dist/
backend/tsconfig.tsbuildinfo
```
Rensa sedan från git-historiken (enbart lokal tracking):
```bash
git rm -r --cached backend/dist/ backend/tsconfig.tsbuildinfo
git commit -m "chore: untrack compiled backend dist files"
git push
```
På servern, kör sedan:
```bash
git checkout -- backend/dist/ backend/tsconfig.tsbuildinfo 2>/dev/null; git pull
```
---
---
## 🔑 **3. .gitignore täcker inte bare `.env` — MEDIUM**
**Problem:** `.gitignore` har `.env.*` (täcker `.env.local`, `.env.production` etc.) men INTE ett eventuellt `.env` utan suffix.
**Lösning:** Lägg till i `.gitignore`:
```gitignore
.env
.env.*
!.env.example
```
**Skapa `.env.example`** (finns ej i repot):
```env
DATABASE_URL=mysql://user:password@localhost:3306/recipe_app
JWT_SECRET=minst-32-tecken-slumpad-sträng
MARIADB_ROOT_PASSWORD=
MARIADB_DATABASE=recipe_app
MISTRAL_API_KEY=
NEXT_PUBLIC_APP_URL=https://recept.example.com
ADMIN_NADMIN_PASSWORD=
ADMIN_PADMIN_PASSWORD=
SEED_USER1_PASSWORD=
SEED_USER2_PASSWORD=
ALLOWED_ORIGIN=https://recept.example.com
RECEIPT_TRACE_DECISIONS=0
```
---
---
## 📱 **4. Flutter — tokenlagring på webben — MEDIUM**
**Nuläge:** `WebTokenStorage` använder `SharedPreferences` som mappar till `localStorage` i webbläsaren. JWT i localStorage är åtkomlig via JavaScript och kan stjälas vid XSS.
**Realistisk åtgärd för en Flutter Web-app:** Det finns inget httpOnly-cookie-alternativ direkt i Flutter Web utan backend-stöd. Om XSS-risken bedöms hög, överväg:
1. Backend sätter token som httpOnly cookie vid login — Flutter Web läser aldrig token direkt
2. Alternativt: sessionStorage (rensas vid stängt fönster) via JS-interop
**Nuvarande abstraktionslager är korrekt strukturerat** (`ITokenStorage` → `WebTokenStorage`). Byt ut implementationen vid behov utan att ändra resten av koden.
> ️ `flutter_secure_storage` fungerar **inte** på web — det är korrekt att web-adaptern använder SharedPreferences.
---
---
## 🔌 **5. Webhook-säkerhet — LOW (om Gitea-webhooks används)**
**Nuläge:** Inga webhook-endpoints hittades i koden. Åtgärd krävs endast om Gitea-webhooks kopplas in.
**Om webhooks läggs till** — validera `X-Gitea-Signature` med `crypto.timingSafeEqual`:
```typescript
// src/webhooks/gitea.controller.ts
import { Controller, Post, Headers, Body, RawBodyRequest, Req, UnauthorizedException } from '@nestjs/common';
import * as crypto from 'crypto';
import { Public } from '../auth/decorators/public.decorator';
@Controller('webhooks/gitea')
export class GiteaController {
@Public()
@Post()
async handleWebhook(
@Headers('x-gitea-signature-256') signature: string,
@Req() req: RawBodyRequest<Request>,
) {
const secret = process.env.GITEA_WEBHOOK_SECRET;
if (!secret) throw new Error('GITEA_WEBHOOK_SECRET saknas');
const hmac = crypto.createHmac('sha256', secret);
hmac.update(req.rawBody!);
const expected = `sha256=${hmac.digest('hex')}`;
if (!crypto.timingSafeEqual(Buffer.from(signature ?? ''), Buffer.from(expected))) {
throw new UnauthorizedException('Ogiltig signatur');
}
// Hantera event...
}
}
```
Lägg till `GITEA_WEBHOOK_SECRET` i `.env.example`.
---
---
## 🧪 **6. CI/CD-säkerhetstester — MEDIUM**
> ️ Projektet använder **Gitea** (inte GitHub), så GitHub Actions-workflows i originalplanen fungerar inte.
Nuvarande CI: tester körs via `npm test` på push (pipeline finns i Gitea).
**Utöka befintlig pipeline** med:
- `npm audit --audit-level=high` — kontrollera kända CVE:er i beroenden
- `npx prisma validate` — verifiera schema-integritet
- Lägg till `gitleaks` som pre-commit hook lokalt (körs inte i Docker-build)
```bash
# Installera som lokal pre-commit hook
npm install -g gitleaks
gitleaks protect --staged # kör före varje commit
```
---
---
## ✅ **7. Avslutande Checklista**
| **Åtgärd** | **Status** | **Ansvarsområde** |
| -------------------------------- | ---------- | ----------------------- |
| IDOR-skydd: recipes, pantry, meal-plan, receipt-alias | ✅ Klart | Backend |
| IDOR-skydd + userId-filtrering för **inventory** | ✅ Klart | Backend (NestJS/Prisma) |
| `dist/` tillagd i `.gitignore` | ✅ Klart | Git |
| `.env` (utan suffix) i `.gitignore` + `.env.example` | ✅ Klart | Git |
| Helmet, CORS, ThrottlerGuard, ValidationPipe | ✅ Klart | Backend |
| Flutter: token-abstraktion (`ITokenStorage`) | ✅ Klart | Flutter |
| Flutter: httpOnly cookie-alternativ (om XSS är reell risk) | ⬜ LOW | Flutter + Backend |
| Gitea webhook-signaturvalidering (om webhooks används) | ⬜ LOW | Backend |
| `npm audit` i CI-pipeline | ✅ Klart | CI/CD (Gitea) |
---
---
## 🎯 **8. Prioriterad Ordning för Implementering**
1. **Inventory IDOR + userId-fält** (CRITICAL) — ✅ KLART 2026-05-07
2. **dist/ i .gitignore** (HIGH) — ✅ KLART 2026-05-07
3. **bare .env + .env.example** (MEDIUM) — ✅ KLART 2026-05-07
4. **npm audit i CI** (MEDIUM) — ✅ KLART 2026-05-07
5. **Flutter httpOnly cookies** (LOW — kräver arkitekturförändring)
6. **Gitea webhook-validering** (LOW — bara relevant om webhooks används)
---
## 2026-05-07: Sammanfattning av senaste säkerhetsförbättringar
- **Inventory är nu user-scopad:** Alla inventory-operationer kräver och filtrerar på userId i backend (schema, migration, service, controller, tester).
- **IDOR-skydd för inventory:** Det är nu omöjligt för användare att läsa eller ändra andras inventarieposter. Tester verifierar att åtkomst nekas vid försök till IDOR.
- **.gitignore och deploy-hygien:** backend/dist och backend/tsconfig.tsbuildinfo ignoreras och är ej längre spårade i git. .env och .env.* ignoreras, men .env.example finns och är uppdaterad.
- **CI/CD-härdning:** npm audit och prisma validate körs i pipeline. Alla tester och byggen måste passera.
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.