@@ -11,7 +11,7 @@ import os from "os";
1111import { requireAuth } from "../middleware/requireAuth.js" ;
1212import { asyncHandler } from "../middleware/asyncHandler.js" ;
1313import { getSpaces , getSpaceById , getSectionById , createAuditLog , getCategoryConfigs } from "../services/db.js" ;
14- import { listFiles , downloadFile , uploadFile , deleteFile , createFolder } from "../services/drive.js" ;
14+ import { listFiles , downloadFile , uploadFile , deleteFile , createFolder , verifyFolderAncestry , verifyFileAncestry } from "../services/drive.js" ;
1515
1616// Allowed MIME types for uploads — documents and common office formats only
1717const ALLOWED_MIME_TYPES = new Set ( [
@@ -87,6 +87,16 @@ export function isAdminUser(groups: string[]): boolean {
8787 return groups . some ( ( g ) => g === "portal_admin" || g === "/portal_admin" ) ;
8888}
8989
90+ // ---------------------------------------------------------------------------
91+ // Helper: validate a user-supplied Drive folder ID
92+ // ---------------------------------------------------------------------------
93+
94+ const DRIVE_ID_PATTERN = / ^ [ a - z A - Z 0 - 9 _ - ] + $ / ;
95+
96+ function isValidDriveId ( id : string ) : boolean {
97+ return DRIVE_ID_PATTERN . test ( id ) && id . length >= 1 && id . length <= 128 ;
98+ }
99+
90100// ---------------------------------------------------------------------------
91101// GET /documents/categories — category sort-order config (auth required, no admin needed)
92102// Used by the spaces listing page to order category sections correctly.
@@ -139,7 +149,21 @@ router.get("/:spaceId", async (req: Request, res: Response): Promise<void> => {
139149
140150 try {
141151 const folderId = req . query . folderId as string | undefined ;
142- const targetFolderId = folderId || space . driveFolderId ;
152+ let targetFolderId = space . driveFolderId ;
153+
154+ if ( folderId ) {
155+ if ( ! isValidDriveId ( folderId ) ) {
156+ res . status ( 400 ) . json ( { error : "Invalid folder ID" , code : "INVALID_FOLDER_ID" } ) ;
157+ return ;
158+ }
159+ const belongs = await verifyFolderAncestry ( folderId , space . driveFolderId ) ;
160+ if ( ! belongs ) {
161+ res . status ( 403 ) . json ( { error : "Folder is not within this space" , code : "FOLDER_OUTSIDE_SPACE" } ) ;
162+ return ;
163+ }
164+ targetFolderId = folderId ;
165+ }
166+
143167 const files = await listFiles ( targetFolderId ) ;
144168 res . json ( { space, files } ) ;
145169 } catch ( err ) {
@@ -211,7 +235,21 @@ router.get(
211235
212236 try {
213237 const folderId = req . query . folderId as string | undefined ;
214- const targetFolderId = folderId || section . driveFolderId ;
238+ let targetFolderId = section . driveFolderId ;
239+
240+ if ( folderId ) {
241+ if ( ! isValidDriveId ( folderId ) ) {
242+ res . status ( 400 ) . json ( { error : "Invalid folder ID" , code : "INVALID_FOLDER_ID" } ) ;
243+ return ;
244+ }
245+ const belongs = await verifyFolderAncestry ( folderId , space . driveFolderId ) ;
246+ if ( ! belongs ) {
247+ res . status ( 403 ) . json ( { error : "Folder is not within this space" , code : "FOLDER_OUTSIDE_SPACE" } ) ;
248+ return ;
249+ }
250+ targetFolderId = folderId ;
251+ }
252+
215253 const files = await listFiles ( targetFolderId ) ;
216254 res . json ( { space, section, files } ) ;
217255 } catch ( err ) {
@@ -253,9 +291,14 @@ router.get(
253291 }
254292
255293 try {
256- const { stream, mimeType, name } = await downloadFile (
257- String ( req . params . fileId ) ,
258- ) ;
294+ const fileId = String ( req . params . fileId ) ;
295+ const fileBelongs = await verifyFileAncestry ( fileId , space . driveFolderId ) ;
296+ if ( ! fileBelongs ) {
297+ res . status ( 403 ) . json ( { error : "File is not within this space" , code : "FILE_OUTSIDE_SPACE" } ) ;
298+ return ;
299+ }
300+
301+ const { stream, mimeType, name } = await downloadFile ( fileId ) ;
259302 // ?download=1 → force browser save-as dialog (attachment) regardless of type.
260303 // Without the flag: PDFs stream inline (so the in-portal PDF viewer can fetch them);
261304 // all other types are always forced to attachment.
@@ -377,12 +420,22 @@ router.post(
377420 }
378421
379422 try {
380- // Priority: 1. folderId (subfolder), 2. sectionId (category folder), 3. space.driveFolderId (root)
381423 const folderId = req . query . folderId as string | undefined ;
382424 const sectionId = req . query . sectionId as string | undefined ;
383425 let targetFolderId = space . driveFolderId ;
384426
385427 if ( folderId ) {
428+ if ( ! isValidDriveId ( folderId ) ) {
429+ fs . unlink ( file . path , ( ) => { } ) ;
430+ res . status ( 400 ) . json ( { error : "Invalid folder ID" , code : "INVALID_FOLDER_ID" } ) ;
431+ return ;
432+ }
433+ const belongs = await verifyFolderAncestry ( folderId , space . driveFolderId ) ;
434+ if ( ! belongs ) {
435+ fs . unlink ( file . path , ( ) => { } ) ;
436+ res . status ( 403 ) . json ( { error : "Folder is not within this space" , code : "FOLDER_OUTSIDE_SPACE" } ) ;
437+ return ;
438+ }
386439 targetFolderId = folderId ;
387440 } else if ( sectionId ) {
388441 const section = await getSectionById ( space . id , sectionId ) ;
@@ -482,6 +535,15 @@ router.post(
482535 let targetFolderId = space . driveFolderId ;
483536
484537 if ( folderId ) {
538+ if ( ! isValidDriveId ( folderId ) ) {
539+ res . status ( 400 ) . json ( { error : "Invalid folder ID" , code : "INVALID_FOLDER_ID" } ) ;
540+ return ;
541+ }
542+ const belongs = await verifyFolderAncestry ( folderId , space . driveFolderId ) ;
543+ if ( ! belongs ) {
544+ res . status ( 403 ) . json ( { error : "Folder is not within this space" , code : "FOLDER_OUTSIDE_SPACE" } ) ;
545+ return ;
546+ }
485547 targetFolderId = folderId ;
486548 } else if ( sectionId ) {
487549 const section = await getSectionById ( space . id , sectionId ) ;
@@ -547,6 +609,12 @@ router.delete(
547609 }
548610
549611 try {
612+ const fileBelongs = await verifyFileAncestry ( fileId , space . driveFolderId ) ;
613+ if ( ! fileBelongs ) {
614+ res . status ( 403 ) . json ( { error : "File is not within this space" , code : "FILE_OUTSIDE_SPACE" } ) ;
615+ return ;
616+ }
617+
550618 await deleteFile ( fileId ) ;
551619
552620 res . status ( 204 ) . end ( ) ;
0 commit comments