mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 09:24:54 +10:00
Project quality audit (#2758)
* Harden security, health checks, and dependency hygiene Co-authored-by: Amruth Pillai <im.amruth@gmail.com> * Finalize health and storage hardening adjustments Co-authored-by: Amruth Pillai <im.amruth@gmail.com> * remove use of [REDACTED] * update dependencies --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
@@ -18,7 +18,7 @@ export const printerRouter = {
|
||||
.input(z.object({ id: z.string().describe("The unique identifier of the resume to export.") }))
|
||||
.output(z.object({ url: z.string().describe("The URL to download the generated PDF file.") }))
|
||||
.handler(async ({ input, context }) => {
|
||||
const { id, data, userId } = await resumeService.getByIdForPrinter({ id: input.id });
|
||||
const { id, data, userId } = await resumeService.getByIdForPrinter({ id: input.id, userId: context.user?.id });
|
||||
const url = await printerService.printResumeAsPDF({ id, data, userId });
|
||||
|
||||
if (!context.user) {
|
||||
@@ -41,9 +41,13 @@ export const printerRouter = {
|
||||
})
|
||||
.input(z.object({ id: z.string().describe("The unique identifier of the resume.") }))
|
||||
.output(z.object({ url: z.string().nullable().describe("The URL to the screenshot image, or null.") }))
|
||||
.handler(async ({ input }) => {
|
||||
.handler(async ({ context, input }) => {
|
||||
try {
|
||||
const { id, data, userId, updatedAt } = await resumeService.getByIdForPrinter({ id: input.id });
|
||||
const { id, data, userId, updatedAt } = await resumeService.getByIdForPrinter({
|
||||
id: input.id,
|
||||
userId: context.user.id,
|
||||
});
|
||||
|
||||
const url = await printerService.getResumeScreenshot({ id, data, userId, updatedAt });
|
||||
|
||||
return { url };
|
||||
|
||||
@@ -11,6 +11,14 @@ const filenameSchema = z.object({
|
||||
filename: z.string().min(1).describe("The path or filename of the file to delete."),
|
||||
});
|
||||
|
||||
function normalizeKey(input: string): string {
|
||||
return input.trim().replace(/^\/+/, "").split("/").filter(Boolean).join("/");
|
||||
}
|
||||
|
||||
function isUnsafeStorageKey(key: string): boolean {
|
||||
return key.split("/").some((segment) => segment === "." || segment === "..");
|
||||
}
|
||||
|
||||
export const storageRouter = {
|
||||
uploadFile: protectedProcedure
|
||||
.route({
|
||||
@@ -76,13 +84,21 @@ export const storageRouter = {
|
||||
message: "The specified file was not found in storage.",
|
||||
status: 404,
|
||||
},
|
||||
FORBIDDEN: {
|
||||
message: "You do not have permission to delete this file.",
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
.handler(async ({ context, input }): Promise<void> => {
|
||||
// The filename is now the full path from the URL (e.g., "uploads/userId/pictures/timestamp.ext")
|
||||
// We need to extract just the path portion that matches the storage key
|
||||
const key = input.filename.startsWith("uploads/")
|
||||
? input.filename
|
||||
: `uploads/${context.user.id}/pictures/${input.filename}`;
|
||||
const requestedKey = normalizeKey(input.filename);
|
||||
const key = requestedKey.startsWith("uploads/")
|
||||
? requestedKey
|
||||
: normalizeKey(`uploads/${context.user.id}/pictures/${requestedKey}`);
|
||||
const userPrefix = `uploads/${context.user.id}/`;
|
||||
|
||||
if (isUnsafeStorageKey(key) || !key.startsWith(userPrefix)) {
|
||||
throw new ORPCError("FORBIDDEN");
|
||||
}
|
||||
|
||||
const deleted = await storageService.delete(key);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user