From a19bc1279a2e8317ca9cb124e17e388765563830 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Thu, 7 May 2026 07:56:49 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20update=20l1Category=20method=20to=20retu?= =?UTF-8?q?rn=20'=C3=96vrigt'=20for=20empty=20categoryPath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Säkerhetshärdningsplan för Recipe-app.md | 729 ++++++++++++++++++ .../inventory/domain/inventory_item.dart | 2 +- 2 files changed, 730 insertions(+), 1 deletion(-) create mode 100644 Säkerhetshärdningsplan för Recipe-app.md diff --git a/Säkerhetshärdningsplan för Recipe-app.md b/Säkerhetshärdningsplan för Recipe-app.md new file mode 100644 index 00000000..49e09c1b --- /dev/null +++ b/Säkerhetshärdningsplan för Recipe-app.md @@ -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:** 1–3 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('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 `). +> - **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); +> } +> } +> ```" +> ``` + +--- + +--- + +## 🔌 **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 { +> 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 +> // ... +> } +> ```" +> ``` + +--- + +--- + +## 🛡️ **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.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 diff --git a/flutter/lib/features/inventory/domain/inventory_item.dart b/flutter/lib/features/inventory/domain/inventory_item.dart index 915d2fbf..4f354e3e 100644 --- a/flutter/lib/features/inventory/domain/inventory_item.dart +++ b/flutter/lib/features/inventory/domain/inventory_item.dart @@ -38,7 +38,7 @@ class InventoryItem { String get l1Category { final path = categoryPath?.trim(); - if (path == null || path.isEmpty) return 'Ovrigt'; + if (path == null || path.isEmpty) return 'Övrigt'; return path.split('>').first.trim(); }