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

21 KiB
Raw Blame History

🔒 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:
    # 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:
    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:

// 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:

// 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:

// 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:

// 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:

// 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:

-- 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:

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:

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:
// 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:

// 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:

// 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:

// 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.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
.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.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:

# 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:

# 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:

// 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:

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:

// 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:

# .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:

# .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:

# 🔒 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?