Add TransferTribe app and fix critical transfer security flaws
The application source was untracked, so this commit brings it under version control together with fixes for the issues found while auditing it. Notable fixes: Authorization - Require a session and scope to senderEmail on /api/transfers/[id] (GET, DELETE) and .../resend. These were unauthenticated over an autoincrement id, so the ids could be walked to soft-delete any transfer, read sender/recipient metadata, or make the app mail arbitrary recipients. - Require a session on the legacy /api/send and /api/chunk-upload endpoints, and take the sender from the session rather than a request field so transfers cannot be posted as another user. Encryption - Replace the chunk encryption scheme. Every chunk was encrypted under one shared session IV with its auth tag discarded, which reuses the AES-GCM keystream (XORing two ciphertexts recovers plaintext without the key) and left the stored file undecryptable, surfacing to users as a wrong-password error. Chunks are now self-contained frames carrying their own random IV and auth tag, behind a magic+salt header. - Files written by the previous format now report UNSUPPORTED_FORMAT instead of a misleading password error. Download - Verify the password against the stored bcrypt hash before serving a file, and enforce expiresAt and DELETED/EXPIRED status. - Move the password from the query string into a POST body so it stays out of access logs and Referer headers. - Record a download only after successful authentication. - Decrypt frame by frame through a stream instead of buffering the whole file, and encode the Content-Disposition filename per RFC 5987. Data exposure - /api/download ran before the password prompt and returned the full transfer row, including absolute server file paths. It now returns only what the pre-password screen renders; filenames, message and recipient are withheld until /api/verify succeeds. Correctness - Fix BigInt handling that made /api/transfers and /api/transfers/[id] fail unconditionally (JSON.stringify cannot serialize BigInt, and seeding a BigInt reduce with 0 throws). - Fail loudly on a missing chunk during reassembly rather than silently writing a corrupt file. - Meter plan usage in plaintext bytes rather than on-disk encrypted size. Ignore /uploads: it holds runtime transfer payloads, not source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum TransferStatus {
|
||||
ACTIVE
|
||||
EXPIRED
|
||||
DELETED
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
plan String @default("free") // "free", "rookie", or "pro"
|
||||
|
||||
// Optional
|
||||
transfers Transfer[] @relation("UserTransfers")
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String?
|
||||
access_token String?
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String?
|
||||
session_state String?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
|
||||
model Transfer {
|
||||
id Int @id @default(autoincrement())
|
||||
senderEmail String
|
||||
recipientEmail String
|
||||
passwordHash String?
|
||||
downloadUrl String @unique
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime
|
||||
files File[]
|
||||
filenames String? // JSON stringified array of filenames
|
||||
totalFiles Int @default(0) // new field to track number of files in transfer
|
||||
totalSize BigInt @default(0) // Use BigInt to support files > 2GB
|
||||
message String? // optional message
|
||||
status TransferStatus @default(ACTIVE) // Added
|
||||
|
||||
userId String? // Change from Int? to String?
|
||||
user User? @relation("UserTransfers", fields: [userId], references: [id])
|
||||
}
|
||||
|
||||
|
||||
model File {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
path String
|
||||
size BigInt // Use BigInt to support files > 2GB
|
||||
encryptionIv String? // 12-byte IV as base64 string for AES-GCM decryption
|
||||
transferId Int
|
||||
transfer Transfer @relation(fields: [transferId], references: [id])
|
||||
downloads FileDownload[] // New
|
||||
}
|
||||
|
||||
model FileDownload {
|
||||
id Int @id @default(autoincrement())
|
||||
fileId Int
|
||||
file File @relation(fields: [fileId], references: [id], onDelete: Cascade)
|
||||
timestamp DateTime @default(now())
|
||||
ip String?
|
||||
userAgent String?
|
||||
}
|
||||
Reference in New Issue
Block a user