Files
recipe-app/Säkerhetshärdningsplan för Recipe-app.md
T
Nils-Johan Gynther a19bc1279a
Test Suite / test (24.15.0) (push) Has been cancelled
fix: update l1Category method to return 'Övrigt' for empty categoryPath
2026-05-07 07:56:49 +02:00

729 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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)
**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:** 13 dagar
---
---
## 📌 **0. Förberedelser**
### 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**
**Mål:** Se till att användare endast kan komma åt sina egna resurser via API:er.
### 1.1. Backend (NestJS/Prisma)
#### **Steg 1: Lägg till ägarskapskontroll i alla endpoints**
**Uppgift för Copilot:**
> "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<string>('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)`."
**Uppgift för Copilot:**
> "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);
> }
> }
> ```"
> ```
#### **Steg 2: Prisma-middleware för automatisk filtrering**
**Uppgift för Copilot:**
> "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 3: Testa för IDOR**
**Uppgift för Copilot:**
> "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);
> });
> });
> ```"
> ```
---
---
## 🗃️ **2. Full Table Dump CRITICAL**
**Mål:** Förhindra att användare kan hämta alla rader från en tabell.
### 2.1. Backend (Prisma/NestJS)
**Uppgift för Copilot:**
> "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
> });
> }
> ```"
> ```
### 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
> });
> ```"
> ```
---
---
## 📱 **3. Flutter-Säkerhet HIGH**
**Mål:** Säkra kommunikation mellan Flutter och backend, samt hantering av känsliga data.
### 3.1. API-Anrop
**Uppgift för Copilot:**
> "Skapa en **Dart-klass** för säkra API-anrop till backend, med:
>
> - **JWT-autentisering** (skicka `Authorization: Bearer <token>`).
> - **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<Map<String, dynamic>> 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<int> _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<void> saveToken(String token) async {
> await _storage.write(key: 'auth_token', value: token);
> }
>
> Future<String?> getToken() async {
> return await _storage.read(key: 'auth_token');
> }
>
> Future<void> 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);
> }
> }
> ```"
> ```
---
---
## 🔌 **4. Webhook-Säkerhet HIGH**
**Mål:** Validera signaturer för alla inkommande webhooks (t.ex. Gitea).
### 4.1. Gitea-Webhooks
**Uppgift för Copilot:**
> "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
> ```"
> ```
---
---
## 🔑 **5. Hemligheter och Kryptering HIGH**
**Mål:** Skydda API-nycklar, databaslösenord, och krypteringsnycklar.
### 5.1. `.env`-filer
**Uppgift för Copilot:**
> "Skapa en `**.gitignore`-fil** som utesluter alla `.env`-filer och känsliga konfigurationsfiler. Exempel:
>
> ```gitignore
> # .gitignore
> .env
> .env.*
> !.env.example
> *.pem
> *.key
> ```"
> ```
**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<string> {
> const encrypted = await openpgp.encrypt({
> message: openpgp.Message.fromText(privateKey),
> passwords: [password],
> });
> return encrypted.toString();
> }
>
> async decryptPrivateKey(encryptedPrivateKey: string, password: string): Promise<string> {
> 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
> // ...
> }
> ```"
> ```
---
---
## 🛡️ **6. Säkerhetsheaders och CSP MEDIUM**
**Mål:** Skydda backend med säkerhetsheaders.
### 6.1. NestJS (Backend)
**Uppgift för Copilot:**
> "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();
> ```"
> ```
---
---
## 🧪 **7. Automatiserade Säkerhetstester MEDIUM**
**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** |
| -------------------------------- | ---------- | ----------------------- |
| 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 |
---
---
## 🎯 **10. 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.11.3).
2. **Gå vidare till Full Table Dump-skydd** (steg 2.12.3).
3. **Implementera Flutter-säkerhet** (steg 3.13.3).
4. **Säkra webhooks** (steg 4.14.2).
5. **Säkra hemligheter** (steg 5.15.3).
6. **Lägg till säkerhetsheaders** (steg 6.1).
7. **Automatisera tester** (steg 7.17.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?