From eebceb298dff6a5a8a3f7f57c968bd07af41cb82 Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 1 Jul 2026 11:03:16 +0200 Subject: [PATCH] =?UTF-8?q?fix(08):=20apply=20code=20review=20findings=20(?= =?UTF-8?q?CR-01,=20CR-02,=20WR-01=E2=80=9305,=20IN-01)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CR-01: fix SSRF bypass — isPrivateIpv6 now delegates ::ffff: to isPrivateIpv4, covering 172.16-31.x and 169.254.x ranges - CR-02: add ParseUUIDPipe to GET /favorites widgetId param + service guard so missing widgetId returns 400 instead of leaking all user favorites - WR-01: link-widget — replace raw 'link.error' key with t('link.error') (4 sites) - WR-02: favorites-widget — fix load-path error to use t('favorites.error') - WR-03: widget-catalog-modal — move aria-hidden from outer wrapper to backdrop - WR-04: calculator — remove duplicate M button (MR clone); MC/MR/M+/M−/MS remain - WR-05: schema — add FavoriteLink→WidgetInstance FK with onDelete:Cascade - IN-01: create-widget.dto.ts — update comment from four to eight supported types Co-Authored-By: Claude Sonnet 4.6 --- apps/api/prisma/schema.prisma | 36 ++++++++++--------- .../src/dashboard/dto/create-widget.dto.ts | 2 +- .../api/src/favorites/favorites.controller.ts | 3 +- apps/api/src/favorites/favorites.service.ts | 3 +- .../src/favorites/icon-discovery.service.ts | 19 +++++++--- .../dashboard/widget-catalog-modal.tsx | 5 ++- .../dashboard/widgets/calculator-widget.tsx | 1 - .../dashboard/widgets/favorites-widget.tsx | 3 +- .../dashboard/widgets/link-widget.tsx | 8 ++--- 9 files changed, 45 insertions(+), 35 deletions(-) diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index e471381..13a19db 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -124,13 +124,14 @@ model DashboardLayout { } model WidgetInstance { - id String @id @default(uuid()) - userId String - tenantId String - widgetType String - config Json @default("{}") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + userId String + tenantId String + widgetType String + config Json @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + favoriteLinks FavoriteLink[] @@index([userId]) @@index([tenantId]) @@ -235,16 +236,17 @@ model SmtpConfig { } model FavoriteLink { - id String @id @default(uuid()) - userId String - tenantId String - widgetId String - title String - url String - iconUrl String? - position Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + userId String + tenantId String + widgetId String + title String + url String + iconUrl String? + position Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + widgetInstance WidgetInstance @relation(fields: [widgetId], references: [id], onDelete: Cascade) @@index([userId]) @@index([tenantId]) diff --git a/apps/api/src/dashboard/dto/create-widget.dto.ts b/apps/api/src/dashboard/dto/create-widget.dto.ts index 47cd881..caad5c6 100644 --- a/apps/api/src/dashboard/dto/create-widget.dto.ts +++ b/apps/api/src/dashboard/dto/create-widget.dto.ts @@ -2,7 +2,7 @@ import { IsIn, IsObject, IsOptional, IsString } from 'class-validator'; /** * DTO for creating a new widget instance on a user's dashboard. - * widgetType must be one of the four supported types. + * widgetType must be one of the eight supported types. * config is optional and defaults to {} on the model. */ export class CreateWidgetDto { diff --git a/apps/api/src/favorites/favorites.controller.ts b/apps/api/src/favorites/favorites.controller.ts index be8c988..355290b 100644 --- a/apps/api/src/favorites/favorites.controller.ts +++ b/apps/api/src/favorites/favorites.controller.ts @@ -5,6 +5,7 @@ import { ForbiddenException, Get, Param, + ParseUUIDPipe, Patch, Post, Query, @@ -47,7 +48,7 @@ export class FavoritesController { @Get() async list( - @Query('widgetId') widgetId: string, + @Query('widgetId', ParseUUIDPipe) widgetId: string, @Req() req: Request, ) { const { userId } = this.extractContext(req); diff --git a/apps/api/src/favorites/favorites.service.ts b/apps/api/src/favorites/favorites.service.ts index 8e4c4a0..8419d26 100644 --- a/apps/api/src/favorites/favorites.service.ts +++ b/apps/api/src/favorites/favorites.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateFavoriteDto } from './dto/create-favorite.dto'; import { UpdateFavoriteDto } from './dto/update-favorite.dto'; @@ -24,6 +24,7 @@ export class FavoritesService { * Scoped by userId AND widgetId (Pitfall 3 — separate widgets must not share links). */ async list(userId: string, widgetId: string) { + if (!widgetId) throw new BadRequestException('widgetId is required'); return this.prisma.favoriteLink.findMany({ where: { userId, widgetId }, orderBy: [{ position: 'asc' }, { title: 'asc' }], diff --git a/apps/api/src/favorites/icon-discovery.service.ts b/apps/api/src/favorites/icon-discovery.service.ts index 9cb9839..6bc565a 100644 --- a/apps/api/src/favorites/icon-discovery.service.ts +++ b/apps/api/src/favorites/icon-discovery.service.ts @@ -55,16 +55,25 @@ function isPrivateIpv4(address: string): boolean { function isPrivateIpv6(address: string): boolean { const lower = address.toLowerCase(); - return ( + if ( lower === '::' || lower === '::1' || lower.startsWith('fc') || lower.startsWith('fd') || lower.startsWith('fe80:') || - lower.startsWith('::ffff:127.') || - lower.startsWith('::ffff:10.') || - lower.startsWith('::ffff:192.168.') - ); + lower.startsWith('ff') + ) { + return true; + } + + // IPv4-mapped IPv6 (::ffff:) — delegate to isPrivateIpv4 to cover all + // RFC 1918 ranges (10.x, 172.16-31.x, 192.168.x) and 169.254.x link-local + const v4MappedMatch = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/); + if (v4MappedMatch) { + return isPrivateIpv4(v4MappedMatch[1]); + } + + return false; } function isPrivateIpAddress(address: string): boolean { diff --git a/apps/web/src/components/dashboard/widget-catalog-modal.tsx b/apps/web/src/components/dashboard/widget-catalog-modal.tsx index fd4987d..972f0c0 100644 --- a/apps/web/src/components/dashboard/widget-catalog-modal.tsx +++ b/apps/web/src/components/dashboard/widget-catalog-modal.tsx @@ -57,10 +57,9 @@ export function WidgetCatalogModal({