From 943e449c979a13b23245e4f958b1a915ba898ac7 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Thu, 7 May 2026 10:48:35 +0200 Subject: [PATCH] docs: update security status in technical documentation for backend and Flutter frontend - Added detailed security status section in backend documentation, outlining implemented security features and remaining risks as of 2026-05-07. - Included security features implemented in Flutter, emphasizing auth-gating, token storage, and limitations regarding web security. --- Säkerhetshärdningsplan för Recipe-app.md | 936 ++++++++--------------- TEKNISK_BESKRIVNING.md | 31 + flutter/teknisk_beskrivning_flutter.md | 25 + 3 files changed, 357 insertions(+), 635 deletions(-) diff --git a/Säkerhetshärdningsplan för Recipe-app.md b/Säkerhetshärdningsplan för Recipe-app.md index 49e09c1b..f0ca1191 100644 --- a/Säkerhetshärdningsplan för Recipe-app.md +++ b/Säkerhetshärdningsplan för Recipe-app.md @@ -1,9 +1,32 @@ # 🔒 Säkerhetshärdningsplan för Recipe-App (Flutter + NestJS + MariaDB) +**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 -**Tidsuppskattning:** 1–3 dagar +**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` på `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` | --- @@ -13,717 +36,360 @@ ### 0.1. Miljö och verktyg -- **Installera säkerhetsverktyg:** - ```bash - # Scanna efter läckta hemligheter i Git - npm install -g gitleaks - gitleaks detect --source . --report-path gitleaks-report.json - - # Scanna Docker-containers - docker scan --file Dockerfile - ``` - **Skapa en `security-audit`-gren:** ```bash git checkout -b security-audit-$(date +%Y%m%d) ``` -- **Dokumentera nuvarande säkerhetsstatus:** - - Lista alla **API-endpoints** (särskilt de som hanterar användardata: `recipes`, `inventory`, `users`). - - Lista alla **databastabeller** och deras innehåll. - - Lista alla **webhooks** (t.ex. Gitea). --- --- -## 🚨 **1. IDOR (Insecure Direct Object Reference) – CRITICAL** +## 🚨 **1. IDOR + Full Table Dump i Inventory — CRITICAL** -**Mål:** Se till att användare endast kan komma åt sina egna resurser via API:er. +**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`. -### 1.1. Backend (NestJS/Prisma) +**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` -#### **Steg 1: Lägg till ägarskapskontroll i alla endpoints** +**Lösning — 3 steg:** -**Uppgift för Copilot:** +### Steg 1: Lägg till `userId` i Prisma-schemat -> "Skapa en **NestJS-guard** som automatiskt kontrollerar att den inloggade användaren äger resursen (t.ex. `Recipe`, `Inventory`). Använd `req.user.id` för att jämföra med `resource.userId`. Exempel: -> -> ```typescript -> // src/guards/ownership.guard.ts -> import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; -> import { Reflector } from '@nestjs/core'; -> -> @Injectable() -> export class OwnershipGuard implements CanActivate { -> constructor(private reflector: Reflector) {} -> -> canActivate(context: ExecutionContext): boolean { -> const requiredResource = this.reflector.get('resource', context.getHandler()); -> if (!requiredResource) return true; // Ingen resurs specificerad = ingen kontroll -> -> const request = context.switchToHttp().getRequest(); -> const resourceId = request.params.id; -> const userId = request.user.id; -> -> // Hämta resursen och kontrollera ägarskap -> // (Anta att vi har en service för detta) -> const resource = await request[requiredResource + 'Service'].findOne(resourceId); -> if (!resource || resource.userId !== userId) { -> throw new ForbiddenException('Du har inte tillgång till denna resurs.'); -> } -> return true; -> } -> } -> ``` -> -> Registrera guarden globalt i `app.module.ts` eller använd den på specifika routes med `@UseGuards(OwnershipGuard)`." +```prisma +// backend/prisma/schema.prisma +model InventoryItem { + id Int @id @default(autoincrement()) + userId Int // NY KOLUMN + productId Int + // ...övriga fält oförändrade... -**Uppgift för Copilot:** + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + consumptions InventoryConsumption[] -> "Uppdatera alla **controller-metoder** som hanterar `Recipe`, `Inventory`, eller `User` för att använda `@UseGuards(OwnershipGuard)` och `@SetMetadata('resource', 'recipe')` (eller motsvarande). Exempel: -> -> ```typescript -> // src/recipes/recipes.controller.ts -> import { Controller, Get, Param, UseGuards, SetMetadata } from '@nestjs/common'; -> import { OwnershipGuard } from '../guards/ownership.guard'; -> -> @Controller('recipes') -> @UseGuards(OwnershipGuard) -> export class RecipesController { -> @Get(':id') -> @SetMetadata('resource', 'recipe') -> async getRecipe(@Param('id') id: number) { -> return this.recipesService.findOne(id); -> } -> } -> ```" -> ``` + @@index([productId]) + @@index([userId]) // NY INDEX +} -#### **Steg 2: Prisma-middleware för automatisk filtrering** +// Lägg till i User-modellen: +model User { + // ... + inventoryItems InventoryItem[] // NY RELATION +} +``` -**Uppgift för Copilot:** +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. -> "Skapa en **Prisma-middleware** som automatiskt lägger till `userId: req.user.id` till alla `findMany`, `findFirst`, `update`, och `delete`-förfrågningar för tabellerna `Recipe` och `Inventory`. Exempel: -> -> ```typescript -> // src/prisma/prisma.service.ts -> import { Injectable, OnModuleInit } from '@nestjs/common'; -> import { PrismaClient } from '@prisma/client'; -> -> @Injectable() -> export class PrismaService extends PrismaClient implements OnModuleInit { -> private userId: number; -> -> setUserId(userId: number) { -> this.userId = userId; -> } -> -> async onModuleInit() { -> await this.$connect(); -> this.$use(async (params, next) => { -> if (this.userId && ['Recipe', 'Inventory'].includes(params.model)) { -> params.where = { ...params.where, userId: this.userId }; -> } -> return next(params); -> }); -> } -> } -> ``` -> -> Uppdatera sedan `OwnershipGuard` för att sätta `prismaService.setUserId(req.user.id)`." +### Steg 2: Uppdatera service-metoderna -#### **Steg 3: Testa för IDOR** +```typescript +// backend/src/inventory/inventory.service.ts -**Uppgift för Copilot:** +// findAll — lägg till userId-parameter och where-klausul +async findAll(userId: number, query?: InventoryQuery) { + const where: Prisma.InventoryItemWhereInput = { userId }; + // ...resten oförändrat... +} -> "Skapa **Jest-tester** för att verifiera att IDOR-skyddet fungerar. Testa: -> -> 1. En användare kan hämta sina egna recept/inventory. -> 2. En användare **kan inte** hämta andras recept/inventory. -> -> Exempel: -> -> ```typescript -> // test/idor.test.ts -> describe('IDOR Protection', () => { -> let app; -> let user1, user2, recipe1; -> -> beforeAll(async () => { -> app = await createTestApp(); -> user1 = await createTestUser(); -> user2 = await createTestUser(); -> recipe1 = await createTestRecipe(user1.id); -> }); -> -> it('should allow user to access their own recipe', async () => { -> const response = await request(app.getHttpServer()) -> .get(`/recipes/${recipe1.id}`) -> .set('Authorization', `Bearer ${user1.token}`); -> expect(response.status).toBe(200); -> }); -> -> it('should block user from accessing another user\'s recipe', async () => { -> const response = await request(app.getHttpServer()) -> .get(`/recipes/${recipe1.id}`) -> .set('Authorization', `Bearer ${user2.token}`); -> expect(response.status).toBe(403); -> }); -> }); -> ```" -> ``` +// 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. Full Table Dump – CRITICAL** +## �️ **2. dist/ saknas i .gitignore — HIGH** -**Mål:** Förhindra att användare kan hämta alla rader från en tabell. +**Problem:** `backend/dist/` och `backend/tsconfig.tsbuildinfo` är committade i repot och orsakar merge-konflikter vid deploy (bekräftat 2026-05-07). -### 2.1. Backend (Prisma/NestJS) +**Lösning:** -**Uppgift för Copilot:** +Lägg till i `.gitignore` (rotnivå): +```gitignore +# Kompilerat backend-output — byggs i Docker, ska ej spåras +backend/dist/ +backend/tsconfig.tsbuildinfo +``` -> "Uppdatera alla **Prisma-förfrågningar** som använder `findMany` för `Recipe`, `Inventory`, eller `User` för att **alltid** inkludera `where: { userId: req.user.id }`. Exempel: -> -> ```typescript -> // src/recipes/recipes.service.ts -> async findAll(userId: number) { -> return this.prisma.recipe.findMany({ -> where: { userId } // Tvinga filtrering -> }); -> } -> ```" -> ``` +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 +``` -### 2.2. MariaDB (RLS-liknande kontroll) - -**Uppgift för Copilot:** - -> "Skapa **MariaDB-vyer** för `recipes` och `inventory` som automatiskt filtrerar på `userId`. Exempel: -> -> ```sql -> -- Skapa en vy för användarens recept -> CREATE VIEW user_recipes AS -> SELECT * FROM recipes WHERE userId = @current_user_id; -> -> -- Skapa en stored procedure för att sätta @current_user_id -> DELIMITER // -> CREATE PROCEDURE SetCurrentUserId(IN p_userId INT) -> BEGIN -> SET @current_user_id = p_userId; -> END // -> DELIMITER ; -> ``` -> -> **Obs:** MariaDB saknar inbyggt RLS, så vyer + stored procedures är det bästa alternativet. Dokumentera att alla förfrågningar ska gå via vyer." - -**Uppgift för Copilot:** - -> "Skapa en **stored procedure** för att hämta användarens inventory, med `userId` som parameter. Exempel: -> -> ```sql -> DELIMITER // -> CREATE PROCEDURE GetUserInventory(IN p_userId INT) -> BEGIN -> SELECT * FROM inventory WHERE userId = p_userId; -> END // -> DELIMITER ; -> ```" -> ``` - -### 2.3. Testa för Full Table Dump - -**Uppgift för Copilot:** - -> "Skapa **Jest-tester** för att verifiera att användare inte kan hämta alla rader. Exempel: -> -> ```typescript -> it('should block full table dump for recipes', async () => { -> const user = await createTestUser(); -> const response = await request(app.getHttpServer()) -> .get('/recipes') -> .set('Authorization', `Bearer ${user.token}`); -> expect(response.body.length).toBeLessThanOrEqual(10); // Anta att användaren har <10 recept -> }); -> ```" -> ``` +På servern, kör sedan: +```bash +git checkout -- backend/dist/ backend/tsconfig.tsbuildinfo 2>/dev/null; git pull +``` --- --- -## 📱 **3. Flutter-Säkerhet – HIGH** +## 🔑 **3. .gitignore täcker inte bare `.env` — MEDIUM** -**Mål:** Säkra kommunikation mellan Flutter och backend, samt hantering av känsliga data. +**Problem:** `.gitignore` har `.env.*` (täcker `.env.local`, `.env.production` etc.) men INTE ett eventuellt `.env` utan suffix. -### 3.1. API-Anrop +**Lösning:** Lägg till i `.gitignore`: +```gitignore +.env +.env.* +!.env.example +``` -**Uppgift för Copilot:** - -> "Skapa en **Dart-klass** för säkra API-anrop till backend, med: -> -> - **JWT-autentisering** (skicka `Authorization: Bearer `). -> - **Validering av svar** (kontrollera att `userId` i svaret matchar den inloggade användaren). -> - **Felhantering** för 403 (Forbidden) och 401 (Unauthorized). -> Exempel: -> -> ```dart -> // lib/services/api_service.dart -> import 'package:http/http.dart' as http; -> import 'dart:convert'; -> -> class ApiService { -> final String baseUrl; -> final String token; -> -> ApiService({required this.baseUrl, required this.token}); -> -> Future> getRecipe(int recipeId) async { -> final response = await http.get( -> Uri.parse('$baseUrl/recipes/$recipeId'), -> headers: {'Authorization': 'Bearer $token'}, -> ); -> -> if (response.statusCode == 403) { -> throw Exception('Du har inte tillgång till detta recept.'); -> } else if (response.statusCode != 200) { -> throw Exception('Fel vid hämtning av recept.'); -> } -> -> final data = json.decode(response.body); -> // Kontrollera att användar-ID matchar (om backend skickar med det) -> if (data['userId'] != await _getCurrentUserId()) { -> throw Exception('Ogiltig användare för detta recept.'); -> } -> return data; -> } -> -> Future _getCurrentUserId() async { -> // Hämta den inloggade användarens ID från lokal lagring -> final user = await _getStoredUser(); -> return user['id']; -> } -> } -> ```" -> ``` - -### 3.2. Lagring av känsliga data - -**Uppgift för Copilot:** - -> "Skapa en **säker lagringslösning** för JWT-tokens och användardata i Flutter med `flutter_secure_storage`. Exempel: -> -> ```dart -> // lib/services/secure_storage.dart -> import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -> -> class SecureStorage { -> final FlutterSecureStorage _storage = const FlutterSecureStorage(); -> -> Future saveToken(String token) async { -> await _storage.write(key: 'auth_token', value: token); -> } -> -> Future getToken() async { -> return await _storage.read(key: 'auth_token'); -> } -> -> Future deleteToken() async { -> await _storage.delete(key: 'auth_token'); -> } -> } -> ```" -> ``` - -### 3.3. Inputvalidering - -**Uppgift för Copilot:** - -> "Skapa en **valideringsklass** för all input i Flutter (t.ex. för recept-ID, användar-ID). Exempel: -> -> ```dart -> // lib/utils/validators.dart -> class Validators { -> static bool isValidId(String id) { -> return RegExp(r'^[0-9]+$').hasMatch(id); -> } -> -> static bool isValidUuid(String uuid) { -> return RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$').hasMatch(uuid); -> } -> } -> ```" -> ``` +**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. Webhook-Säkerhet – HIGH** +## 📱 **4. Flutter — tokenlagring på webben — MEDIUM** -**Mål:** Validera signaturer för alla inkommande webhooks (t.ex. Gitea). +**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. -### 4.1. Gitea-Webhooks +**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 -**Uppgift för Copilot:** +**Nuvarande abstraktionslager är korrekt strukturerat** (`ITokenStorage` → `WebTokenStorage`). Byt ut implementationen vid behov utan att ändra resten av koden. -> "Skapa en **NestJS-controller** för Gitea-webhooks som validerar `X-Gitea-Signature`-headern. Använd `crypto` för att verifiera HMAC-SHA256-signaturen. Exempel: -> -> ```typescript -> // src/webhooks/gitea.controller.ts -> import { Controller, Post, Headers, Body, UnauthorizedException } from '@nestjs/common'; -> import * as crypto from 'crypto'; -> -> @Controller('webhooks/gitea') -> export class GiteaController { -> @Post() -> async handleWebhook( -> @Headers('X-Gitea-Signature') signature: string, -> @Body() body: any, -> ) { -> const secret = process.env.GITEA_WEBHOOK_SECRET; -> const hmac = crypto.createHmac('sha256', secret); -> hmac.update(JSON.stringify(body)); -> const expectedSignature = `sha256=${hmac.digest('hex')}`; -> -> if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) { -> throw new UnauthorizedException('Ogiltig signatur'); -> } -> -> // Hantera webhook-händelsen (t.ex. triggera backup) -> return { status: 'success' }; -> } -> } -> ```" -> ``` - -### 4.2. Miljövariabler - -**Uppgift för Copilot:** - -> "Lägg till `GITEA_WEBHOOK_SECRET` i `.env.example` och dokumentera att den **måste** sättas i produktion. Exempel: -> -> ```env -> # .env.example -> GITEA_WEBHOOK_SECRET=your_gitea_webhook_secret_here -> ```" -> ``` +> ℹ️ `flutter_secure_storage` fungerar **inte** på web — det är korrekt att web-adaptern använder SharedPreferences. --- --- -## 🔑 **5. Hemligheter och Kryptering – HIGH** +## 🔌 **5. Webhook-säkerhet — LOW (om Gitea-webhooks används)** -**Mål:** Skydda API-nycklar, databaslösenord, och krypteringsnycklar. +**Nuläge:** Inga webhook-endpoints hittades i koden. Åtgärd krävs endast om Gitea-webhooks kopplas in. -### 5.1. `.env`-filer +**Om webhooks läggs till** — validera `X-Gitea-Signature` med `crypto.timingSafeEqual`: -**Uppgift för Copilot:** +```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'; -> "Skapa en `**.gitignore`-fil** som utesluter alla `.env`-filer och känsliga konfigurationsfiler. Exempel: -> -> ```gitignore -> # .gitignore -> .env -> .env.* -> !.env.example -> *.pem -> *.key -> ```" -> ``` +@Controller('webhooks/gitea') +export class GiteaController { + @Public() + @Post() + async handleWebhook( + @Headers('x-gitea-signature-256') signature: string, + @Req() req: RawBodyRequest, + ) { + 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... + } +} +``` -**Uppgift för Copilot:** - -> "Skapa en `**.env.example`-fil** med alla nödvändiga miljövariabler (utan värden). Exempel: -> -> ```env -> # .env.example -> DATABASE_URL=mysql://user:password@localhost:3306/recipe_app -> JWT_SECRET=your_jwt_secret_here -> GITEA_WEBHOOK_SECRET=your_gitea_webhook_secret_here -> PGP_PRIVATE_KEY_ENCRYPTION_PASSWORD=your_password_here -> ```" -> ``` - -### 5.2. Docker - -**Uppgift för Copilot:** - -> "Uppdatera `Dockerfile` och `docker-compose.yml` för att: -> -> 1. **Ta bort alla `ENV`-instruktioner** som exponerar känsliga data. -> 2. Använd `docker run --env-file` eller Docker Secrets för känsliga variabler. -> -> Exempel: -> -> ```yaml -> # docker-compose.yml -> services: -> app: -> build: . -> env_file: -> - .env -> ```" -> ``` - -**Uppgift för Copilot:** - -> "Skapa en `**docker-compose.override.yml**` för lokal utveckling, som aldrig pushas till Git. Exempel: -> -> ```yaml -> # docker-compose.override.yml -> services: -> app: -> environment: -> - NODE_ENV=development -> - DATABASE_URL=mysql://dev_user:dev_password@db:3306/recipe_app -> ```" -> ``` - -### 5.3. PGP/AES-Kryptering - -**Uppgift för Copilot:** - -> "Skapa en **tjänst för kryptering/avkryptering** av PGP-nycklar med `openpgp`. Exempel: -> -> ```typescript -> // src/crypto/crypto.service.ts -> import { Injectable } from '@nestjs/common'; -> import * as openpgp from 'openpgp'; -> -> @Injectable() -> export class CryptoService { -> async encryptPrivateKey(privateKey: string, password: string): Promise { -> const encrypted = await openpgp.encrypt({ -> message: openpgp.Message.fromText(privateKey), -> passwords: [password], -> }); -> return encrypted.toString(); -> } -> -> async decryptPrivateKey(encryptedPrivateKey: string, password: string): Promise { -> const message = await openpgp.readMessage({ armoredMessage: encryptedPrivateKey }); -> const { data: decrypted } = await openpgp.decrypt({ -> message, -> passwords: [password], -> }); -> return decrypted.toString(); -> } -> } -> ```" -> ``` - -**Uppgift för Copilot:** - -> "Uppdatera **Prisma-schemat** för att lagra `encryptedPrivateKey` istället för `privateKey` i klart text. Exempel: -> -> ```prisma -> model User { -> id Int @id @default(autoincrement()) -> encryptedPrivateKey String // Krypterad PGP-privat nyckel -> // ... -> } -> ```" -> ``` +Lägg till `GITEA_WEBHOOK_SECRET` i `.env.example`. --- --- -## 🛡️ **6. Säkerhetsheaders och CSP – MEDIUM** +## 🧪 **6. CI/CD-säkerhetstester — MEDIUM** -**Mål:** Skydda backend med säkerhetsheaders. +> ℹ️ Projektet använder **Gitea** (inte GitHub), så GitHub Actions-workflows i originalplanen fungerar inte. -### 6.1. NestJS (Backend) +Nuvarande CI: tester körs via `npm test` på push (pipeline finns i Gitea). -**Uppgift för Copilot:** +**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) -> "Konfigurera **Helmet** i `main.ts` för att lägga till säkerhetsheaders. Exempel: -> -> ```typescript -> // src/main.ts -> import { NestFactory } from '@nestjs/core'; -> import { AppModule } from './app.module'; -> import * as helmet from 'helmet'; -> -> async function bootstrap() { -> const app = await NestFactory.create(AppModule); -> app.use(helmet()); -> await app.listen(3000); -> } -> bootstrap(); -> ```" -> ``` +```bash +# Installera som lokal pre-commit hook +npm install -g gitleaks +gitleaks protect --staged # kör före varje commit +``` --- --- -## 🧪 **7. Automatiserade Säkerhetstester – MEDIUM** +## ✅ **7. Avslutande Checklista** -**Mål:** Integrera säkerhetstester i CI/CD. - -### 7.1. GitHub Actions - -**Uppgift för Copilot:** - -> "Skapa en **GitHub Actions-workflow** för säkerhetskontroller. Exempel: -> -> ```yaml -> # .github/workflows/security-audit.yml -> name: Security Audit -> on: [push, pull_request] -> -> jobs: -> gitleaks: -> runs-on: ubuntu-latest -> steps: -> - uses: actions/checkout@v4 -> - uses: gitleaks/gitleaks-action@v2 -> env: -> GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -> -> docker-scan: -> runs-on: ubuntu-latest -> steps: -> - uses: actions/checkout@v4 -> - name: Build Docker image -> run: docker build -t recipe-app . -> - name: Scan for vulnerabilities -> run: docker scan recipe-app -> ```" -> ``` - -### 7.2. OWASP ZAP - -**Uppgift för Copilot:** - -> "Skapa en **GitHub Actions-workflow** för OWASP ZAP-scanning. Exempel: -> -> ```yaml -> # .github/workflows/zap-scan.yml -> name: OWASP ZAP Scan -> on: [push] -> -> jobs: -> zap_scan: -> runs-on: ubuntu-latest -> steps: -> - uses: actions/checkout@v4 -> - name: Start app -> run: | -> docker-compose up -d -> sleep 30 # Vänta på att appen ska starta -> - name: Run OWASP ZAP -> uses: zaproxy/action-full-scan@v0.4.0 -> with: -> target: 'http://localhost:3000' -> ```" -> ``` - ---- - ---- - -## 📝 **8. Dokumentation** - -**Uppgift för Copilot:** - -> "Skapa en `**SECURITY.md**`-fil i rotkatalogen med: -> -> 1. En lista över alla säkerhetsåtgärder som vidtagits. -> 2. Instruktioner för hur man rapporterar säkerhetshål. -> 3. En checklist för säkerhetsgranskning före deployment. -> -> Exempel: -> -> ```markdown -> # 🔒 Säkerhetsdokumentation -> -> ## Vidtagna åtgärder -> - [x] IDOR-skydd för alla API-endpoints -> - [x] Full Table Dump-skydd via Prisma-middleware -> - [x] Webhook-signaturvalidering för Gitea -> - [x] `.env`-filer uteslutna från Git -> - [x] Flutter: Säker lagring av JWT-tokens -> -> ## Rapportera säkerhetshål -> Skicka ett e-post till security@recipe-app.com. -> -> ## Checklist före deployment -> - [ ] Alla `.env`-filer är uteslutna från Git. -> - [ ] Alla API-endpoints har ägarskapskontroll. -> - [ ] Webhook-signaturer valideras. -> - [ ] Docker-containers är scannade för sårbarheter. -> - [ ] Flutter-app använder `flutter_secure_storage` för tokens. -> ```" -> ``` - ---- - ---- - -## ✅ **9. Avslutande Checklista** - - -| **Åtgärd** | **Status** | **Ansvarsområde** | +| **Åtgärd** | **Status** | **Ansvarsområde** | | -------------------------------- | ---------- | ----------------------- | -| IDOR-skydd i alla endpoints | ⬜ | Backend (NestJS/Prisma) | -| Full Table Dump-skydd | ⬜ | Backend (Prisma) | -| Flutter: Säkra API-anrop | ⬜ | Flutter | -| Flutter: Säker lagring av tokens | ⬜ | Flutter | -| Webhook-signaturvalidering | ⬜ | Backend (NestJS) | -| `.env`-filer skyddade | ⬜ | Git/Docker | -| Docker-säkerhet | ⬜ | Docker | -| PGP-nycklar krypterade | ⬜ | Backend (CryptoService) | -| Säkerhetsheaders (Helmet) | ⬜ | Backend (NestJS) | -| Automatiserade säkerhetstester | ⬜ | CI/CD (GitHub Actions) | -| Säkerhetsdokumentation | ⬜ | Dokumentation | - +| IDOR-skydd: recipes, pantry, meal-plan, receipt-alias | ✅ Klart | Backend | +| IDOR-skydd + userId-filtrering för **inventory** | ⬜ **CRITICAL** | Backend (NestJS/Prisma) | +| `dist/` tillagd i `.gitignore` | ⬜ **HIGH** | Git | +| `.env` (utan suffix) i `.gitignore` + `.env.example` | ⬜ MEDIUM | 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 | ⬜ MEDIUM | CI/CD (Gitea) | --- --- -## 🎯 **10. Prioriterad Ordning för Implementering** +## 🎯 **8. Prioriterad Ordning för Implementering** -1. **IDOR-skydd** (CRITICAL) -2. **Full Table Dump-skydd** (CRITICAL) -3. **Flutter-säkerhet** (HIGH) -4. **Webhook-säkerhet** (HIGH) -5. **Hemligheter och kryptering** (HIGH) -6. **Säkerhetsheaders** (MEDIUM) -7. **Automatiserade tester** (MEDIUM) - ---- - ---- - -## 💡 **11. Tips för GitHub Copilot** - -- **Var specifik:** Beskriv exakt vad du vill uppnå (t.ex. "Skapa en NestJS-guard för ägarskapskontroll"). -- **Ge exempel:** Inkludera kodsnuttar för att visa vad du menar. -- **Be om tester:** Fråga Copilot att skapa **Jest-tester** för varje säkerhetsåtgärd. -- **Iterera:** Om svaret inte är perfekt, be Copilot att **förbättra** eller **förtydliga** koden. - ---- - ---- - -## 🚀 **12. Nästa Steg** - -1. **Börja med IDOR-skyddet** (steg 1.1–1.3). -2. **Gå vidare till Full Table Dump-skydd** (steg 2.1–2.3). -3. **Implementera Flutter-säkerhet** (steg 3.1–3.3). -4. **Säkra webhooks** (steg 4.1–4.2). -5. **Säkra hemligheter** (steg 5.1–5.3). -6. **Lägg till säkerhetsheaders** (steg 6.1). -7. **Automatisera tester** (steg 7.1–7.2). -8. **Dokumentera** (steg 8). - ---- - -**Fråga till dig:** -Vill du att jag **anpassar planen ytterligare** för ett specifikt område (t.ex. endast backend eller endast Flutter)? Eller ska vi börja med **IDOR-skyddet** först? \ No newline at end of file +1. **Inventory IDOR + userId-fält** (CRITICAL) — påverkar alla användares data +2. **dist/ i .gitignore** (HIGH) — förhindrar återkommande deploy-problem +3. **bare .env + .env.example** (MEDIUM) +4. **npm audit i CI** (MEDIUM) +5. **Flutter httpOnly cookies** (LOW — kräver arkitekturförändring) +6. **Gitea webhook-validering** (LOW — bara relevant om webhooks används) diff --git a/TEKNISK_BESKRIVNING.md b/TEKNISK_BESKRIVNING.md index 047c273f..f2610aee 100644 --- a/TEKNISK_BESKRIVNING.md +++ b/TEKNISK_BESKRIVNING.md @@ -95,6 +95,37 @@ Detta dokument är skrivet för systemadministratörer och programmerare. Fokus ### Driftnotering Verifiera efter deploy att seed-körning inkluderar uppdaterat kategoriträd och att kvittoflödet använder den senaste regelbaserade parserlogiken. +## Sakerhetsstatus (2026-05-07) + +Denna sektion beskriver sakerhetsfunktioner som ar implementerade i kodbasen och hur de ar implementerade tekniskt. + +### Implementerat i backend + +- JWT-skydd globalt: `JwtAuthGuard` ar registrerad som global APP_GUARD i `backend/src/app.module.ts`. Alla endpoints kravs pa giltig bearer-token om de inte ar markerade med `@Public()`. +- Rollbaserad access: `RolesGuard` ar global guard och laser metadata fran `@Roles(...)`. Endpoints med adminkrav blockerar icke-admin med `ForbiddenException`. +- Rate limiting: `ThrottlerGuard` ar globalt aktiv med standard 120 anrop/minut. Login/register har hardare throttle (`10/min`) i `auth.controller.ts`. +- Input-hardening: global `ValidationPipe` i `backend/src/main.ts` med `whitelist: true`, `forbidNonWhitelisted: true`, `transform: true`. + Detta tar bort odefinierade falt och nekar payloads med extra fields. +- Security headers: `helmet(...)` ar aktiverat i `backend/src/main.ts` med HSTS, `x-frame-options`, `x-content-type-options`, referrer policy och cross-origin policies. +- CORS-begransning: origin satts via `ALLOWED_ORIGIN` (fallback host i kod) och credentials ar explicit styrda i `main.ts`. +- Losenordshashning: `bcryptjs` med kostnad 12 i `auth.service.ts` (`bcrypt.hash(password, 12)`). +- User-scope i flera domaner: + - `recipes`: controllers/services skickar vidare `@CurrentUser().userId` och filtrerar pa agarskap. + - `pantry` och `meal-plan`: alla las/skriv-operationer ar user-scopade via `userId` i service-lagret. + - `receipt-alias`: las/skriv/radera styrs av owner/global-regler och aktuell anvandare. + +### Delvis implementerat / kvarvarande risk + +- Inventory ar inte fullstandigt user-scopat: + - `InventoryItem` saknar `userId` i `backend/prisma/schema.prisma`. + - `inventory.controller.ts` anvander inte `@CurrentUser()` for filtrering/auktorisering. + - Detta ar en kvarvarande IDOR/Full Table Dump-risk tills inventory ar migrerat till user-scope. + +### Driftrelaterad hardening som finns + +- Secrets injiceras via miljo i `compose.yml` (t.ex. `JWT_SECRET`, `DATABASE_URL`, API-nycklar) och ar inte hardkodade i appkod. +- Prisma migrationer kor vid containerstart och recovery-playbook for P3009 finns dokumenterad i detta dokument. + ## Översikt Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och matplanering. Systemet är byggt med Next.js (frontend), NestJS (backend), Prisma ORM och MariaDB. Applikationen är containeriserad med Docker och använder Caddy som reverse proxy. diff --git a/flutter/teknisk_beskrivning_flutter.md b/flutter/teknisk_beskrivning_flutter.md index eae12e30..a28d15b7 100644 --- a/flutter/teknisk_beskrivning_flutter.md +++ b/flutter/teknisk_beskrivning_flutter.md @@ -44,6 +44,31 @@ Bygg och drift sker pa Linux/Ubuntu i containeriserad miljo. - Auth-gate i router med redirect logik - `guardedApiCall()` hanterar logout vid 401 +## Sakerhetsstatus (2026-05-07) + +Denna sektion sammanfattar sakerhetsfunktioner som ar implementerade i Flutter-klienten och hur de fungerar tekniskt. + +### Implementerat i Flutter + +- Auth-gating i routing: + - Router-lagret stoppar navigation till skyddade vyer utan token/session. + - Om backend returnerar 401 i ett skyddat API-flode hanterar `guardedApiCall()` detta och triggar logout/omdirigering. +- Token-lagring via plattformsabstraktion: + - `ITokenStorage` definierar kontraktet (`getToken/saveToken/deleteToken`). + - Web-implementation (`WebTokenStorage`) lagrar token i `SharedPreferences` (web: localStorage). + - Arkitekturen gor att mobil implementation kan bytas till `flutter_secure_storage` utan att ovrig appkod andras. +- Minimerad klientside-auktorisering: + - Flutter forlitar sig pa backend som auktoritetskalla for access-kontroll. + - Klienten skickar bearer-token men gor inte egen resurstagarskapslogik som kan divergera fran serverregler. +- Kontraktsstyrd API-hantering: + - API-lagret accepterar 2xx pa importanrop och centraliserar felhantering. + - Minskar risken for ad-hoc felhantering i UI och inkonsekvent beteende vid auth-fel. + +### Viktig begransning (web) + +- Flutter Web kan inte anvanda `httpOnly` cookies enbart pa klientsidan. +- Nuvarande lagring i localStorage ar en praktisk kompromiss for web och innebar att XSS-hardening pa frontend och strict backend-headers/CSP ar fortsatt viktiga. + ## API- och kontraktsprinciper - Flutter foljer backend-kontrakt, ingen lokal speciallogik for matchning.