Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix file name parsing logic for image deletion #1264

Merged
merged 2 commits into from
Jan 27, 2025
Merged
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
8 changes: 4 additions & 4 deletions src/app/api/user/bulkImportTicks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export interface Tick {
notes: string
climbId: string
userId: string | undefined
style: string
attemptType: string
style: string | null
attemptType: string | null
dateClimbed: Date
grade: string
source: string
Expand Down Expand Up @@ -99,8 +99,8 @@ const postHandler = async (req: NextRequest): Promise<any> => {
notes: tick.Notes,
climbId: tick.mp_id,
userId: uuid,
style: tick.Style === '' ? 'N/A' : tick.Style,
attemptType: tick.Style === '' ? 'N/A' : tick.Style,
style: tick.Style === '' ? null : tick.Style,
attemptType: tick['Lead Style'] === '' ? null : tick['Lead Style'],
dateClimbed: new Date(Date.parse(`${tick.Date}T00:00:00`)), // Date.parse without timezone specified converts date to user's present timezone.
grade: tick.Rating,
source: 'MP'
Expand Down
47 changes: 41 additions & 6 deletions src/app/api/user/get-signed-url/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { customAlphabet } from 'nanoid'
import { nolookalikesSafe } from 'nanoid-dictionary'
import { extname } from 'path'
import { basename, extname } from 'path'

import { withUserAuth, PREDEFINED_HEADERS } from '@/js/auth/withUserAuth'
import { getSignedUrlForUpload } from '@/js/media/storageClient'
Expand Down Expand Up @@ -36,30 +36,65 @@ export const GET = withUserAuth(getHanlder)
/**
* Random filename generator
*/
const safeFilename = (original: string): string => {
const safeRandomFilename = (original: string): string => {
return safeRandomString() + extname(original)
}

const safeRandomString = customAlphabet(nolookalikesSafe, 10)

/**
* Construct the S3 path for a given media file and an authenticated user. Format: `u/{user_uuid}/{filename}`.
* It's super important **not** to have the leading slash '/'.
* Extract filename and uuid of to-be-deleted media
* @param req NextRequest
* @returns [filename, uuid] or null if missing any of the params
*/
export const prepareFilenameFromRequest = (req: NextRequest): string | null => {
const extractFilenameAndUuidFromAuthenticatedRequest = (req: NextRequest): [string, string] | null => {
const searchParams = req.nextUrl.searchParams
const filename = searchParams.get('filename')
if (filename == null) {
return null
}

/**
* Extract uuid from http-header of authenticated request
* because we can't trust the uuid in the query string
*/
const uuid = req.headers.get(PREDEFINED_HEADERS.user_uuid)
if (uuid == null) {
return null
}
return [filename, uuid]
}

/* Construct an S3 path for a given media file and an authenticated user. Format: `u/{user_uuid}/{filename}`.
* It's super important **not** to have the leading slash '/'.
*/
export const prepareFilenameFromRequest = (req: NextRequest): string | null => {
const data = extractFilenameAndUuidFromAuthenticatedRequest(req)
if (data == null) {
return null
}
const filename = data[0]
const uuid = data[1]
/**
* Important: no starting slash / when working with buckets
*/
return `u/${uuid}/${safeRandomFilename(filename)}`
}

/**
* For a given query string `?filename=u/{user_uuid}/{filename}` return `u/{user_uuid}/filename` by
* resconstructing the path with uuid from the header to prevent spoofing attack.
* @see withUserAuth
*/
export const getBucketPathFromRequest = (req: NextRequest): string | null => {
const data = extractFilenameAndUuidFromAuthenticatedRequest(req)
if (data == null) {
return null
}
const filename = data[0]
const uuid = data[1]
/**
* Important: no starting slash / when working with buckets
*/
return `u/${uuid}/${safeFilename(filename)}`
return `u/${uuid}/${basename(filename)}`
}
4 changes: 2 additions & 2 deletions src/app/api/user/remove-media/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { NextRequest, NextResponse } from 'next/server'

import { withUserAuth } from '@/js/auth/withUserAuth'
import { deleteMediaFromBucket } from '@/js/media/storageClient'
import { prepareFilenameFromRequest } from '../get-signed-url/route'
import { getBucketPathFromRequest } from '../get-signed-url/route'

/**
* Endpoint for removing a media object from remote cloud storage
*/
const postHandler = async (req: NextRequest): Promise<any> => {
try {
const filename = prepareFilenameFromRequest(req)
const filename = getBucketPathFromRequest(req)
if (filename == null) {
return NextResponse.json({ status: 400 })
}
Expand Down
Loading