feat(teams): harden the upload path for an uploader who is not an admin
The existing admin upload path is already good for an admin: an 8 MB cap, a mimetype allowlist, a random filename, an extension derived from the mimetype map and never from originalname, and nosniff forced on serve. All of it is kept. What it does not have is anything that assumes a hostile uploader, because until now it has not had one. Magic-byte sniffing, because `file.mimetype` is the client's own Content-Type header — a player can send image/png with arbitrary bytes and land arbitrary content under a .png. Unrecognised bytes are a rejection and never a fallback to what the header claimed. The file is on disk before it can be sniffed, so the rejection path removes it: a rejected upload left on disk is the same disk-exhaustion vector reached another way. A rolling per-account byte quota and a per-IP rate limit, because community uploads with no ceiling is disk exhaustion on the operator's own host. An attribution row per accepted file. Not bookkeeping: the acknowledgement is meaningless if "who uploaded this" cannot be answered afterwards, which is exactly what the operator has just accepted responsibility for. A nightly sweep for soft-deleted files past retention and for never-referenced orphans, in the same in-process shape as the activity prune. It runs whether or not `uploads` is the current mode, and that is the point — an operator who turns uploads off after a problem still has the files, and a sweep that switched itself off with the setting would strand exactly the bytes they were trying to be rid of. It works from the forum's own rows outward and never from the directory listing inward, because UPLOAD_DIR is shared with the admin upload path. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ const http = require('http')
|
||||
const botScore = require('./middleware/botScore')
|
||||
const announceWorker = require('./utils/announceWorker')
|
||||
const teamActivityPrune = require('./utils/teamActivityPrune')
|
||||
const teamForumUploadSweep = require('./utils/teamForumUploadSweep')
|
||||
const { ensureSchema, close } = require('./utils/db')
|
||||
const { seedDefaults, createInitialAdminFromEnv } = require('../db/seed')
|
||||
const settings = require('./model/settings/settings.model')
|
||||
@@ -155,6 +156,7 @@ async function start() {
|
||||
// is the obvious unbounded-growth failure, so retention starts with the feed
|
||||
// rather than after someone notices. No-op on a deployment with no Teams.
|
||||
teamActivityPrune.start()
|
||||
teamForumUploadSweep.start()
|
||||
|
||||
setupShutdown(server, internalServer)
|
||||
}
|
||||
@@ -174,6 +176,7 @@ function setupShutdown(server, internalServer) {
|
||||
botScore.stopSweeper() // stop the bot-store cleanup interval
|
||||
announceWorker.stop() // stop the news-announcement dispatcher poller
|
||||
teamActivityPrune.stop() // stop the Team activity retention timer
|
||||
teamForumUploadSweep.stop() // stop the forum upload sweep
|
||||
server.close(() => log.info('http server closed'))
|
||||
if (internalServer) internalServer.close(() => log.info('internal http server closed'))
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user