fix(08): apply code review findings (CR-01, CR-02, WR-01–05, IN-01)
- CR-01: fix SSRF bypass — isPrivateIpv6 now delegates ::ffff:<ipv4> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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' }],
|
||||
|
||||
@@ -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:<ipv4>) — 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 {
|
||||
|
||||
Reference in New Issue
Block a user