Overview
In CVE-2026-34784, Parse Server allowed file downloads via HTTP Range requests to bypass the afterFind(Parse.File) trigger and its validators on streaming storage adapters like GridFS. This is a broken function-level authorization issue: the access control logic that should run when a function is invoked can be bypassed by certain streaming paths. The vulnerability existed in versions prior to 8.6.71 and 9.7.1-alpha.1 and was fixed in those releases.
Attackers could issue a Range request to a protected file and start streaming data before the authorization and in-situ validators finished executing, effectively bypassing the afterFind hooks that normally enforce requireUser or other guards. The bypass relied on how streaming paths sidestep function-level authorization in some storage adapters.
In a Node.js/Express app, a similar flaw can arise if a route streams file data without performing per-resource authorization before opening a read stream. Range-based requests can compound the issue by removing the need to reach higher-level guards before delivery begins. The fix is to enforce authorization at the entry point of the streaming path, not solely in model hooks or background validators.
Remediation pattern: upgrade to the patched Parse Server versions (8.6.71 and 9.7.1-alpha.1) and apply equivalent hardening in your own Express apps by validating permissions before streaming and by centralizing access checks. Use signed URLs or short-lived tokens for downloads and add tests that simulate Range requests against protected resources.
Affected Versions
Parse Server versions prior to 8.6.71 and 9.7.1-alpha.1
Code Fix Example
Node.js (Express) API Security Remediation
/* Vulnerable pattern and fix side-by-side for Node.js/Express */
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
// Mock in-memory file metadata store (for illustration)
const files = {
'1': { id: '1', path: path.join(__dirname, 'demo.txt'), size: 1024, ownerId: 'alice' }
};
// Helper: parse Range header
function parseRange(range, size) {
if (!range) return null;
const m = range.match(/bytes=(\\d+)-(\\d+)?/);
if (!m) return null;
const start = parseInt(m[1], 10);
const end = m[2] ? parseInt(m[2], 10) : size - 1;
return { start, end };
}
/* Vulnerable pattern: streaming without any authorization checks */
app.get('/files/:id', (req, res) => {
const f = files[req.params.id];
if (!f) return res.status(404).end();
const range = parseRange(req.headers.range, f.size);
const start = range ? range.start : 0;
const end = range ? range.end : f.size - 1;
res.status(206);
res.set({
'Content-Type': 'application/octet-stream',
'Content-Range': `bytes ${start}-${end}/${f.size}`
});
fs.createReadStream(f.path, { start, end }).pipe(res);
});
/* Fixed version: enforce authentication and authorization before streaming */
function ensureAuthenticated(req, res, next) {
// Simple header-based auth for illustration; replace with real auth in production
if (!req.headers['x-user-id']) return res.status(401).send('Unauthorized');
req.user = { id: req.headers['x-user-id'] };
next();
}
function userHasAccess(user, file) {
return user && file && user.id === file.ownerId;
}
app.get('/files-fixed/:id', ensureAuthenticated, (req, res) => {
const f = files[req.params.id];
if (!f) return res.status(404).end();
if (!userHasAccess(req.user, f)) return res.status(403).send('Forbidden');
const range = parseRange(req.headers.range, f.size);
const start = range ? range.start : 0;
const end = range ? range.end : f.size - 1;
res.status(206);
res.set({
'Content-Type': 'application/octet-stream',
'Content-Range': `bytes ${start}-${end}/${f.size}`
});
fs.createReadStream(f.path, { start, end }).pipe(res);
});
app.listen(3000, () => console.log('Server running on port 3000'));