From f8352cfe5baa695bd2672e0f4be6f70c750cba98 Mon Sep 17 00:00:00 2001 From: 2alf Date: Mon, 22 Dec 2025 19:35:41 +0100 Subject: [PATCH 1/3] XSS fix - Bug: xss vector in the "Target URL *" field on `/requests`. - Fix: added a regex validator on submition for http and https only for leaks.ts and requests.ts --- convex/leaks.ts | 7 +++++++ convex/requests.ts | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/convex/leaks.ts b/convex/leaks.ts index 445e91e..92bb903 100644 --- a/convex/leaks.ts +++ b/convex/leaks.ts @@ -236,6 +236,13 @@ export const insertLeak = mutation({ }; } + if (args.url && !/^https?:\/\/.+/i.test(args.url)) { + return { + success: false as const, + error: "Invalid URL.", + }; + } + // Validate required fields if (!args.targetName || !args.provider || !args.leakText) { return { diff --git a/convex/requests.ts b/convex/requests.ts index 326dbff..47c581a 100644 --- a/convex/requests.ts +++ b/convex/requests.ts @@ -3,6 +3,13 @@ import { getAuthUserId } from "@convex-dev/auth/server"; import { query, mutation } from "./_generated/server"; import { Id } from "./_generated/dataModel"; +// url validation helper +function isValidUrl(url: string): boolean { + return /^https?:\/\/.+/i.test(url); // only http and https +} + + + /** * Get all open (non-closed) requests from the database. * Returns requests with submitter names populated. @@ -104,6 +111,7 @@ export const getUserOpenRequests = query({ }, }); + /** * Create a new request for a leak target. * Validates that: @@ -147,6 +155,13 @@ export const createRequest = mutation({ }; } + if (!isValidUrl(args.targetUrl)) { + return { + success: false as const, + error: "The provided target URL is not valid. Please provide a valid URL starting with http:// or https://", + }; + } + // Check if a request with the same target name already exists (case-insensitive) // Query ALL requests, not just open ones const allRequests = await ctx.db.query("requests").collect(); From 1ee81a4e05b5f4759adfcbcdce3c000c8757ae35 Mon Sep 17 00:00:00 2001 From: 2alf Date: Wed, 24 Dec 2025 21:15:42 +0100 Subject: [PATCH 2/3] X-Frame headers added a _headers to prevent the site to be iframed. --- .gitignore | 2 +- public/_headers | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 public/_headers diff --git a/.gitignore b/.gitignore index 3d7f46f..66758ca 100644 --- a/.gitignore +++ b/.gitignore @@ -86,7 +86,7 @@ jspm_packages/ # Gatsby files .cache/ -public + # Storybook build outputs .out diff --git a/public/_headers b/public/_headers new file mode 100644 index 0000000..8aa4d44 --- /dev/null +++ b/public/_headers @@ -0,0 +1,3 @@ +/* + X-Frame-Options: DENY + Content-Security-Policy: frame-ancestors 'none' \ No newline at end of file From 427a047984ac4c30f6089091f12530e3b6cc6808 Mon Sep 17 00:00:00 2001 From: 2alf Date: Wed, 24 Dec 2025 21:36:56 +0100 Subject: [PATCH 3/3] User-closed request cron job 1. Added `closedBy` field to schema which can be either "user" | "verification" 2. When user closes request => closedBy: "user" 3. When verification closes request => closedBy: "verification" 4. Cron only deletes requests where closedBy === "user" after 1 day Reduces clustering in UI and database. --- convex/crons.ts | 12 ++++++++++++ convex/leaks.ts | 1 + convex/requests.ts | 43 ++++++++++++++++++++++++++++++++++++++++--- convex/schema.ts | 1 + 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 convex/crons.ts diff --git a/convex/crons.ts b/convex/crons.ts new file mode 100644 index 0000000..1964000 --- /dev/null +++ b/convex/crons.ts @@ -0,0 +1,12 @@ +import { cronJobs } from "convex/server"; +import { internal } from "./_generated/api"; + +const crons = cronJobs(); + +crons.daily( + "delete closed requests after a day", + { hourUTC: 1, minuteUTC: 0 }, + internal.requests.deleteOldClosedRequests +); + +export default crons; \ No newline at end of file diff --git a/convex/leaks.ts b/convex/leaks.ts index 92bb903..55bcebf 100644 --- a/convex/leaks.ts +++ b/convex/leaks.ts @@ -544,6 +544,7 @@ export const applyConsensusResult = internalMutation({ await ctx.db.patch(args.requestId, { closed: true, + closedBy: "verification", }); const submitter = await ctx.db.get(leak.submittedBy); diff --git a/convex/requests.ts b/convex/requests.ts index 47c581a..e314ddf 100644 --- a/convex/requests.ts +++ b/convex/requests.ts @@ -1,6 +1,6 @@ import { v } from "convex/values"; import { getAuthUserId } from "@convex-dev/auth/server"; -import { query, mutation } from "./_generated/server"; +import { query, mutation, internalMutation } from "./_generated/server"; import { Id } from "./_generated/dataModel"; // url validation helper @@ -8,8 +8,6 @@ function isValidUrl(url: string): boolean { return /^https?:\/\/.+/i.test(url); // only http and https } - - /** * Get all open (non-closed) requests from the database. * Returns requests with submitter names populated. @@ -279,6 +277,7 @@ export const closeRequest = mutation({ // Close the request await ctx.db.patch(args.requestId, { closed: true, + closedBy: "user", }); return { success: true as const }; @@ -454,3 +453,41 @@ export const getRequestsWithVerificationStatus = query({ return requestsWithStatus; }, }); + +/* + Delete closed requests after a day --> internal mutation called by a cron job +*/ + +export const deleteOldClosedRequests = internalMutation({ + args: {}, + returns: v.number(), + handler: async (ctx) => { + + //const daysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds + const daysAgo = Date.now() - 24 * 60 * 60 * 1000; // 1 day in milliseconds + + const oldClosedRequests = await ctx.db + .query("requests") + .withIndex("by_closed", (q) => q.eq("closed", true)) + .collect(); + + let deletedCount = 0; + + for (const request of oldClosedRequests) { + if (request._creationTime < daysAgo && request.closedBy === "user") { + // remove from users array of requests + if (request.submittedBy) { + const user = await ctx.db.get(request.submittedBy); + if (user && user.requests) { + await ctx.db.patch(request.submittedBy, { + requests: user.requests.filter((id) => id !== request._id), + }); + } + } + await ctx.db.delete(request._id); + deletedCount++; + } + } + return deletedCount; + }, +}); diff --git a/convex/schema.ts b/convex/schema.ts index dda21dd..d119a62 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -43,6 +43,7 @@ const requests = defineTable({ ), targetUrl: v.string(), closed: v.boolean(), + closedBy: v.optional(v.union(v.literal("user"), v.literal("verification"))), leaks: v.array(v.id("leaks")), submittedBy: v.id("users"), })