security
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# File Drop
|
||||
# File Drop (VYBECODED)
|
||||
|
||||
A minimal, self-hosted private file hosting service built with **Bun** and
|
||||
**TypeScript**. Upload a file, get a secret, unguessable download link. Files are
|
||||
@@ -10,6 +10,7 @@ database, no object storage, no queue, and no automatic cleanup.
|
||||
## Features
|
||||
|
||||
- Drag & drop / file-picker uploads with progress, straight from the browser.
|
||||
- **Optional per-file password**: protect a download link with a password.
|
||||
- Permanent storage on the local filesystem; files survive restarts.
|
||||
- A cryptographically secure, unguessable download key for every file.
|
||||
- Streaming uploads and downloads — no full buffering in RAM, large files work.
|
||||
@@ -68,7 +69,7 @@ Then open http://localhost:3000.
|
||||
3. The server stores it permanently and returns a URL like
|
||||
`https://files.example.com/d/8fJ...random-secret...`.
|
||||
4. Copy the URL. Anyone possessing it can download the file; nobody without the
|
||||
secret can.
|
||||
secret can. If a password was set, the downloader must also enter it.
|
||||
5. The file stays available indefinitely. It is removed only by explicit
|
||||
deletion.
|
||||
|
||||
@@ -76,6 +77,7 @@ Then open http://localhost:3000.
|
||||
|
||||
```
|
||||
storage/
|
||||
├── .secret # server signing secret (0600), auto-generated
|
||||
├── files/
|
||||
│ ├── <file-id> # the raw uploaded bytes
|
||||
│ └── <file-id>
|
||||
@@ -98,7 +100,10 @@ Example metadata record (`storage/metadata/<file-id>.json`):
|
||||
```
|
||||
|
||||
Only a SHA-256 **hash** of the download key is stored — the plaintext key exists
|
||||
only inside the URL shown to the uploader.
|
||||
only inside the URL shown to the uploader. If a download password is set, only
|
||||
its scrypt hash is stored (see [password protection](#password-protection)).
|
||||
The server signing secret for download cookies lives in `storage/.secret`
|
||||
(`0600`), created on first start and reused across restarts.
|
||||
|
||||
Uploads are first streamed to a `<file-id>.part` temp file and atomically renamed
|
||||
into place on completion. At startup the server sweeps for orphans (a file
|
||||
@@ -110,10 +115,12 @@ touched.
|
||||
|
||||
### `POST /api/upload`
|
||||
|
||||
Multipart/form-data with a `file` field (this is what the web UI uses).
|
||||
Multipart/form-data with a `file` field (this is what the web UI uses). An
|
||||
optional `password` field protects the download with a password.
|
||||
|
||||
```bash
|
||||
curl -F "[email protected]" http://localhost:3000/api/upload
|
||||
curl -F "password=hunter2" -F "[email protected]" http://localhost:3000/api/upload
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -122,19 +129,39 @@ curl -F "[email protected]" http://localhost:3000/api/upload
|
||||
"key": "YOWJyhDXxhe3-L8MpEFBbAN_e93AqTrEMYutkmStScq5VfbQlrqUt9yHkqmeRp7OQ",
|
||||
"url": "/d/YOWJyhDXxhe3-L8MpEFBbAN_e93AqTrEMYutkmStScq5VfbQlrqUt9yHkqmeRp7OQ",
|
||||
"originalName": "example.zip",
|
||||
"size": 123456
|
||||
"size": 123456,
|
||||
"protected": false
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /d/:key` (also `HEAD`)
|
||||
|
||||
Streams the file. Requires the full secret key; any wrong or non-existent key
|
||||
returns a generic `404`.
|
||||
returns a generic `404`. If the file has a password, the server first shows a
|
||||
password prompt (see below).
|
||||
|
||||
```bash
|
||||
curl -O http://localhost:3000/d/YOWJyh...7OQ
|
||||
```
|
||||
|
||||
### Password protection
|
||||
|
||||
For files uploaded with a `password` field:
|
||||
|
||||
1. `GET /d/:key` returns a small HTML password form.
|
||||
2. The browser submits it to `POST /api/auth/:key` with `password=...`.
|
||||
3. On success the server sets a short-lived (30 min), HttpOnly, SameSite=Lax,
|
||||
signed cookie and redirects back to `/d/:key`, which then streams the file.
|
||||
|
||||
```bash
|
||||
curl -c cookies.txt -X POST -d "password=hunter2" http://localhost:3000/api/auth/:key
|
||||
curl -b cookies.txt -O http://localhost:3000/d/:key
|
||||
```
|
||||
|
||||
The cookie is signed with a server secret (`storage/.secret`) so it cannot be
|
||||
forged, and it survives server restarts. Without it, a protected file is never
|
||||
downloadable — not even by the uploader.
|
||||
|
||||
### `DELETE /api/files/:id`
|
||||
|
||||
Permanently deletes the file and its metadata. The correct download key must be
|
||||
@@ -164,6 +191,9 @@ paths or stack traces.
|
||||
or filenames are ever used as secrets.
|
||||
- **At-rest.** Only a SHA-256 hash of the key is persisted. Comparisons use
|
||||
constant-time `timingSafeEqual`.
|
||||
- **Passwords.** An optional per-file password is stored only as an **scrypt**
|
||||
hash (salted), never in plaintext. Successful entry mints a short-lived
|
||||
HMAC-signed cookie, so access is granted by the server and cannot be forged.
|
||||
- **Path safety.** Filesystem paths are built only from server-generated,
|
||||
regex-validated ids (`^[A-Za-z0-9_-]{22}$`). The original filename is metadata
|
||||
only and is sanitized (basename, control/null bytes stripped) before being
|
||||
@@ -314,4 +344,4 @@ src/
|
||||
```
|
||||
|
||||
Intentionally small: no framework, no DI, no ORM, no repositories. Bun +
|
||||
TypeScript + filesystem + a simple frontend + secret URLs.
|
||||
TypeScript + filesystem + a simple frontend + secret URLs.
|
||||
|
||||
Reference in New Issue
Block a user