termbeam 1.2.9 → 1.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/routes.js +28 -0
package/README.md
CHANGED
|
@@ -128,7 +128,9 @@ Environment variables: `PORT`, `TERMBEAM_PASSWORD`, `TERMBEAM_CWD`, `TERMBEAM_LO
|
|
|
128
128
|
|
|
129
129
|
TermBeam auto-generates a password and creates a tunnel by default, so your terminal is protected out of the box. By default, the server binds to `127.0.0.1` (localhost only). Use `--lan` or `--host 0.0.0.0` to allow LAN access, or `--no-tunnel` to disable the tunnel.
|
|
130
130
|
|
|
131
|
-
Auth uses secure httpOnly cookies with 24-hour expiry, login is rate-limited to 5 attempts per minute, and security headers (X-Frame-Options, X-Content-Type-Options, etc.) are set on all responses. The QR code on startup embeds a share token for password-free login — the token is reusable within its 5-minute validity window, which handles tunnel proxy retries and link preview services. API clients that can't use cookies can authenticate with an `Authorization: Bearer <password>` header.
|
|
131
|
+
Auth uses secure httpOnly cookies with 24-hour expiry, login is rate-limited to 5 attempts per minute, and security headers (X-Frame-Options, X-Content-Type-Options, etc.) are set on all responses. The QR code on startup embeds a share token for password-free login — the token is reusable within its 5-minute validity window, which handles tunnel proxy retries and link preview services. API clients that can't use cookies can authenticate with an `Authorization: Bearer <password>` header.
|
|
132
|
+
|
|
133
|
+
For the full threat model, safe usage guidance, and a quick safety checklist, see [SECURITY.md](SECURITY.md). For detailed security feature documentation, see the [Security Guide](https://dorlugasigal.github.io/TermBeam/security/).
|
|
132
134
|
|
|
133
135
|
## Contributing
|
|
134
136
|
|
package/package.json
CHANGED
package/src/routes.js
CHANGED
|
@@ -9,6 +9,30 @@ const log = require('./logger');
|
|
|
9
9
|
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
|
|
10
10
|
const uploadedFiles = new Map(); // id -> filepath
|
|
11
11
|
|
|
12
|
+
const IMAGE_SIGNATURES = [
|
|
13
|
+
{ type: 'image/png', bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },
|
|
14
|
+
{ type: 'image/jpeg', bytes: [0xff, 0xd8, 0xff] },
|
|
15
|
+
{ type: 'image/gif', bytes: [0x47, 0x49, 0x46, 0x38] },
|
|
16
|
+
{ type: 'image/webp', offset: 8, bytes: [0x57, 0x45, 0x42, 0x50] },
|
|
17
|
+
{ type: 'image/bmp', bytes: [0x42, 0x4d] },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function validateMagicBytes(buffer, contentType) {
|
|
21
|
+
const sig = IMAGE_SIGNATURES.find((s) => s.type === contentType);
|
|
22
|
+
if (!sig) return true; // unknown type, skip validation
|
|
23
|
+
const offset = sig.offset || 0;
|
|
24
|
+
if (buffer.length < offset + sig.bytes.length) return false;
|
|
25
|
+
const match = sig.bytes.every((b, i) => buffer[offset + i] === b);
|
|
26
|
+
if (!match) return false;
|
|
27
|
+
// WebP requires RIFF header at offset 0
|
|
28
|
+
if (contentType === 'image/webp') {
|
|
29
|
+
const riff = [0x52, 0x49, 0x46, 0x46];
|
|
30
|
+
if (buffer.length < 4) return false;
|
|
31
|
+
return riff.every((b, i) => buffer[i] === b);
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
function setupRoutes(app, { auth, sessions, config, state }) {
|
|
13
37
|
// Serve static files (manifest.json, sw.js, icons, etc.)
|
|
14
38
|
app.use(express.static(PUBLIC_DIR, { index: false }));
|
|
@@ -204,6 +228,10 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
204
228
|
if (!buffer.length) {
|
|
205
229
|
return res.status(400).json({ error: 'No image data' });
|
|
206
230
|
}
|
|
231
|
+
if (!validateMagicBytes(buffer, contentType)) {
|
|
232
|
+
log.warn(`Upload rejected: content-type "${contentType}" does not match file signature`);
|
|
233
|
+
return res.status(400).json({ error: 'File content does not match declared image type' });
|
|
234
|
+
}
|
|
207
235
|
const ext =
|
|
208
236
|
{
|
|
209
237
|
'image/png': '.png',
|