This commit is contained in:
2026-08-07 22:08:53 +02:00
parent feb408338a
commit 4fd15ec338
10 changed files with 113 additions and 45 deletions

View File

@@ -355,6 +355,9 @@ export default router => {
// Update Default Upload Visibility preference
group.put(/\/default_upload_visibility/, lib.loggedin, async (req, res) => {
if (cfg.allow_user_upload_visibility === false || cfg.websrv?.allow_user_upload_visibility === false) {
return res.json({ success: false, msg: 'Custom upload visibility is disabled by the administrator' }, 403);
}
const vis = parseInt(req.post.default_upload_visibility, 10);
if (isNaN(vis) || ![0, 1, 2].includes(vis)) {
return res.json({ success: false, msg: 'Invalid visibility option' }, 400);

View File

@@ -139,12 +139,25 @@ import { getManualApproval, getMinTags, getBypassDuplicateCheck } from "../../se
const getTargetVisibility = (req, postVis) => {
if (cfg.enable_private_uploads === false) return 0;
const sysDefault = (typeof cfg.default_upload_visibility === 'number')
? cfg.default_upload_visibility
: (typeof cfg.websrv?.default_upload_visibility === 'number' ? cfg.websrv.default_upload_visibility : 0);
const allowUserOverride = cfg.allow_user_upload_visibility !== false && cfg.websrv?.allow_user_upload_visibility !== false;
if (!allowUserOverride) {
return sysDefault;
}
const rawHeader = req.headers ? req.headers['x-upload-visibility'] : null;
const val = (rawHeader || postVis || '').toString().trim().toLowerCase();
if (val === 'private' || val === '2') return 2;
if (val === 'unlisted' || val === '1') return 1;
if (val === 'public' || val === '0') return 0;
return req.session?.default_upload_visibility || 0;
return (req.session?.default_upload_visibility !== undefined && req.session?.default_upload_visibility !== null)
? req.session.default_upload_visibility
: sysDefault;
};
// Collect request body as buffer with debug logging

View File

@@ -875,13 +875,28 @@ process.on('uncaughtException', err => {
}
});
// CSRF validation helper — used by route handlers that have already populated req.session
// NOTE: Cannot be used in flummpress app.use() middlewares for upload/avatar bypass handlers
// because flummpress runs ALL middlewares in parallel (Promise.all), so the session
// middleware hasn't finished by the time these run. Those handlers validate CSRF inline.
const validateCsrf = (req, res) => {
// Intercept app.readBody to allow memoized reading of request body.
// flummpress runs app.use() in parallel via Promise.all before routing/body-reading,
// so validateCsrf needs to be able to read req.post without breaking subsequent router handler body parsing.
const _originalReadBody = app.readBody.bind(app);
app.readBody = async (req) => {
if (req.post !== undefined) return req.post;
return await _originalReadBody(req);
};
// CSRF validation helper — used by route handlers and global middleware
const validateCsrf = async (req, res) => {
if (req.session && req.session.csrf_token) {
const token = req.headers['x-csrf-token'] || req.body?.csrf_token || req.post?.csrf_token || req.url.qs?.csrf_token;
let token = req.headers['x-csrf-token'] || req.body?.csrf_token || req.post?.csrf_token || req.url.qs?.csrf_token;
// If header/query token is missing and body is not parsed yet on a non-GET method, parse it now
if (!token && req.post === undefined && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
try {
req.post = await app.readBody(req);
token = req.post?.csrf_token;
} catch (e) {}
}
if (!token || token !== req.session.csrf_token) {
console.error(`[CSRF] Blocked ${req.method} ${req.url.pathname} for user ${req.session.user}. Reason: ${!token ? 'Missing token' : 'Token mismatch'}`);
res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg: 'Invalid CSRF token' }));
@@ -903,7 +918,7 @@ process.on('uncaughtException', err => {
if (cfg.websrv.halls_enabled !== false && req.url.pathname.match(/^\/api\/v2\/admin\/halls(\/|$)/)) return;
// User hall image upload is handled by bypass middleware below
if (cfg.websrv.userhalls_enabled !== false && cfg.websrv.enable_userhall_image_upload !== false && req.url.pathname.match(/^\/api\/v2\/me\/halls\/[^/]+\/image$/)) return;
if (!validateCsrf(req, res)) return;
if (!(await validateCsrf(req, res))) return;
});
// Bypass middleware for direct upload handling
@@ -1363,6 +1378,8 @@ process.on('uncaughtException', err => {
site_description: cfg.websrv.description || "The webs dumpster",
enable_nsfl: !!cfg.enable_nsfl,
enable_private_uploads: cfg.enable_private_uploads !== false,
default_upload_visibility: (typeof cfg.default_upload_visibility === 'number' ? cfg.default_upload_visibility : (typeof cfg.websrv?.default_upload_visibility === 'number' ? cfg.websrv.default_upload_visibility : 0)),
allow_user_upload_visibility: cfg.allow_user_upload_visibility !== false && cfg.websrv?.allow_user_upload_visibility !== false,
nsfl_tag_id: cfg.nsfl_tag_id || 3,
scroller_mime_cats: Array.isArray(cfg.allowedMimes) ? cfg.allowedMimes.filter(c => ['video','image','audio'].includes(c)) : ['video','image','audio'],
themes_json: JSON.stringify(cfg.websrv.themes || []),

View File

@@ -145,18 +145,30 @@ export const handleUpload = async (req, res, self) => {
// Parse visibility: Header 'X-Upload-Visibility' or body field 'visibility' or user default preference
let targetVisibility = 0;
if (cfg.enable_private_uploads !== false) {
const rawVisHeader = req.headers['x-upload-visibility'];
const rawVisBody = parts.visibility;
const visVal = (rawVisHeader || rawVisBody || '').toString().trim().toLowerCase();
const sysDefault = (typeof cfg.default_upload_visibility === 'number')
? cfg.default_upload_visibility
: (typeof cfg.websrv?.default_upload_visibility === 'number' ? cfg.websrv.default_upload_visibility : 0);
if (visVal === 'private' || visVal === '2') {
targetVisibility = 2;
} else if (visVal === 'unlisted' || visVal === '1') {
targetVisibility = 1;
} else if (visVal === 'public' || visVal === '0') {
targetVisibility = 0;
const allowUserOverride = cfg.allow_user_upload_visibility !== false && cfg.websrv?.allow_user_upload_visibility !== false;
if (!allowUserOverride) {
targetVisibility = sysDefault;
} else {
targetVisibility = req.session?.default_upload_visibility || 0;
const rawVisHeader = req.headers['x-upload-visibility'];
const rawVisBody = parts.visibility;
const visVal = (rawVisHeader || rawVisBody || '').toString().trim().toLowerCase();
if (visVal === 'private' || visVal === '2') {
targetVisibility = 2;
} else if (visVal === 'unlisted' || visVal === '1') {
targetVisibility = 1;
} else if (visVal === 'public' || visVal === '0') {
targetVisibility = 0;
} else {
targetVisibility = (req.session?.default_upload_visibility !== undefined && req.session?.default_upload_visibility !== null)
? req.session.default_upload_visibility
: sysDefault;
}
}
}