Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
## 2025-02-18 - Custom Regex Sanitization Risks
**Vulnerability:** The codebase used a custom regex-based implementation (`lib/security/sanitize-html.ts`) for HTML sanitization, which is fragile and prone to XSS bypasses (e.g. unquoted attributes).
**Learning:** Developers often underestimate the complexity of HTML parsing. Relying on custom regex creates a false sense of security.
**Prevention:** Always use established, battle-tested libraries like `isomorphic-dompurify` for HTML sanitization. Ensure `jsdom` is added as a production dependency for `isomorphic-dompurify` to work correctly in SSR environments.

## 2025-03-08 - Unsafe SVG Rendering via dangerouslySetInnerHTML
**Vulnerability:** User-controlled SVG strings (`avatar.svg_data`) were being injected directly into the DOM using `dangerouslySetInnerHTML`. An attacker could inject an SVG containing embedded malicious `<script>` tags or `onload` event handlers, leading to Stored Cross-Site Scripting (XSS).
**Learning:** Rendering raw SVGs inline is inherently dangerous. SVGs are essentially XML documents capable of embedding JavaScript.
**Prevention:** When you need to display user-provided SVGs and you don't need to manipulate their internal paths via CSS/JS, do not use `dangerouslySetInnerHTML`. Instead, render the SVG securely by encoding it as a data URI (`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgData)}`) and using it as the `src` attribute of a standard `<img>` tag. This forces the browser to treat the SVG strictly as an image, entirely disabling any embedded script execution.
## 2024-05-20 - Unvalidated File Types on Presigned URLs
**Vulnerability:** The presigned URL generation endpoint (`app/api/upload/presigned/route.ts`) checks file size manually, but bypasses the central `validateFile` function. It does not validate `fileName` extensions against `DANGEROUS_EXTENSIONS` or `fileType` against allowed mime types. This can allow users to request signed URLs to upload arbitrary and dangerous file types (like `.exe` or `.html`) directly to the storage bucket, bypassing client-side checks.
**Learning:** Even when delegating the actual upload to a third-party service via presigned URLs, the endpoint generating the URL must still perform strict validation on the proposed file metadata (name and type), as attackers can trivially bypass client-side checks to request a signature for a malicious payload.
**Prevention:** Always use centralized validation utilities (like `validateFile` from `lib/constants/upload.ts`) on all file upload paths, whether handling the stream directly or issuing a presigned URL.
Comment on lines +2 to +4
18 changes: 15 additions & 3 deletions app/api/upload/presigned/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { NextRequest, NextResponse } from "next/server";
import { b2 } from "@/lib/backblaze";
import { requireUploadAccess } from "@/lib/security/upload-access";
import { safeServerError } from "@/lib/security/route-guards";
import {
validateFile,
ALLOWED_GENERAL_TYPES,
MAX_GENERAL_SIZE,
} from "@/lib/constants/upload";

export async function POST(request: NextRequest) {
try {
Expand Down Expand Up @@ -32,9 +37,16 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Valid fileSize is required" }, { status: 400 });
}

const MAX_FILE_SIZE = 40 * 1024 * 1024;
if (fileSize > MAX_FILE_SIZE) {
return NextResponse.json({ error: "File too large" }, { status: 400 });
const validation = validateFile(
fileName,
fileSize,
fileType,
ALLOWED_GENERAL_TYPES,
MAX_GENERAL_SIZE
);
Comment on lines +40 to +46

if (!validation.valid) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
Comment on lines +40 to 50

const presignedData = await b2.getPresignedUploadUrl(
Expand Down