fix: update l1Category method to return 'Övrigt' for empty categoryPath
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-07 07:56:49 +02:00
parent 7f7e4c24a8
commit a19bc1279a
2 changed files with 730 additions and 1 deletions
+729
View File
@@ -0,0 +1,729 @@
# 🔒 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?