Read this page before you ship a FilesCollection to production. Defaults favor getting started quickly, not locked-down access.
protected: true only checks that the visitor is logged in. Any signed-in user can download any file if they know or guess its URL (an IDOR risk). Pass a function to compare the file owner with the current user:
import { FilesCollection } from 'meteor/ostrio:files';
const files = new FilesCollection({
collectionName: 'files',
protected(fileObj) {
// `fileObj` is `null` if the file does not exist
if (fileObj && fileObj.userId && fileObj.userId === this.userId) {
return true;
}
// `false` replies with 401, a number replies with that status code
return 403;
},
});fileObj.userId is set on upload to the user who uploaded the file. Use the meta field for sharing rules, e.g. fileObj.meta.sharedWith.includes(this.userId).
Uploads are anonymous unless onBeforeUpload checks the user. Check this.userId, and validate size and type:
const files = new FilesCollection({
collectionName: 'files',
onBeforeUpload(fileData) {
if (!this.userId) {
return 'Sign in to upload files';
}
if (fileData.size > 10485760) {
return 'Max size is 10MB';
}
if (!/^(png|jpe?g)$/i.test(fileData.extension)) {
return 'Only PNG and JPEG files are allowed';
}
return true;
},
});The server enforces these rules for in-progress uploads:
- Only the user who started an upload can send chunks, the end-of-file message, or abort it. Others get
403. Aborting an unknown or foreign upload gets404. Uploads started without a logged-in user are anonymous and belong to any anonymous caller, so checkthis.userIdinonBeforeUploadwhen that matters - The server ignores client-supplied reserved file fields:
_id,fileId,path,_storagePath,_downloadRoute,_collectionName,versions,userId,public, the extension and mime fields, and the type flags (isVideo,isImage, and others) chunkSizemust be an integer from 1 byte to 16 MiB. The declaredsizeand the chunk count must agree, chunks must be in range, and the total written can not exceed the declaredsize. The storedsizeis the real size of the file on disk- HTTP request bodies are limited: 1 MiB for Start (including
meta), 64 KiB for EOF, and the base64 size ofchunkSizeplus 4 KiB for a chunk. Larger bodies get413 - Start returns
409if the target file already exists, if the file id is already in use, or if another pending upload claims the same path. The server creates the file exclusively, so it never overwrites an existing file - File names from
namingFunctionare sanitized per path segment, and the final path must resolve inside thestoragePathof the collection. Otherwise Start returns400 - The server remembers the device and inode of the file it created. If the file is removed or replaced before the upload ends, the upload fails with
410or409and the other file is not touched - The abort call removes only the unfinished upload, never a finished file
- A repeated EOF returns the stored file record only to the authenticated user who owns the upload. Anonymous callers get
408 - Responses to the client never contain server paths (
path,_storagePath,versions.*.path), and HTTP errors with5xxstatus do not expose internal messages - An open file handle of an idle upload is closed after
uploadIdleTimeout
Client-supplied values such as type, name, and meta are still untrusted. Verify content in onAfterUpload, for example with the file-type package.
allowClientCode is true by default. Without onBeforeRemove, any client can call removeAsync() and delete any file. Do one of these:
// Option 1: no remove from client code at all
const files = new FilesCollection({
collectionName: 'files',
allowClientCode: false,
});
// Option 2: allow removal and check the owner
const files2 = new FilesCollection({
collectionName: 'files2',
async onBeforeRemove(cursor) {
if (!this.userId) {
return false;
}
const records = await cursor.fetchAsync();
return records.every((file) => file.userId === this.userId);
},
});When allowClientCode is true and onBeforeRemove is not set, the server prints a warning at start. The default changes to false in v4.
Also call denyClient() on the server, or define your own allow/deny rules, so clients can not write to the underlying Mongo.Collection directly.
serve() and unlinkAsync() trust the file paths stored in documents (versions.*.path). The server verifies that paths stay inside storagePath only when it creates an upload, not later. allowClient() lets any client update documents, including path, so a client could point a document at another file on the server and then download or delete it. The same applies to any allow rule or method that lets users write path or versions fields. Paths you pass to addFile(), writeAsync(), and loadAsync() are trusted too. Never build them from user input.
HTTP routes identify the user with the x_mtok cookie. The client sets it to the current DDP session id, and the server maps it to a user.
- The mapping uses the in-memory DDP sessions of one server process. With several instances behind a load balancer, use sticky sessions, or set the
getUseroption to resolve the user another way (a token or your own cookie) - The browser sets the cookie from JavaScript, so it is not
httpOnlyand scripts on your page can read it. It hassameSite: 'Lax'and, in production, thesecureflag. Prevent XSS on your pages - The client sets the cookie to the session id of
Meteor.connection, also when a collection uses a customddpconnection allowQueryStringCookiesputs the token in URLs, where it can leak to logs, proxies, andRefererheaders. Enable it only for Cordova and Meteor-Desktop, and setallowedCordovaOriginswith it. Do not enable it for web apps
The uploader supplies the file type, and the server uses it as the Content-Type of the response. A file labeled text/html is rendered by the browser, which can lead to stored XSS. Reduce the risk:
- Set
nosniff: trueto addX-Content-Type-Options: nosniffto responses. It defaults tofalsein 3.x and will default totruein v4 - Validate the type and extension in
onBeforeUpload, and verify real content inonAfterUpload - Serve untrusted files as downloads. Link with
?download=true, or returnContent-Disposition: attachmentfromresponseHeaders - Serve user files from a separate domain when possible
uploadIdleTimeout(milliseconds, default900000) closes the file handle of an idle upload. The next chunk reopens itcontinueUploadTTL(seconds, default10800) expires unfinished uploadsdisableUpload: trueordisableDownload: trueturn off a direction you do not useallowedOriginscontrols CORS for download routes. Keep it restricted