Skip to content

Commit dc189b8

Browse files
committed
fix: Streaming file download bypasses afterFind(Parse.File) trigger and validators
1 parent 2f7dd2e commit dc189b8

File tree

2 files changed

+94
-4
lines changed

2 files changed

+94
-4
lines changed

spec/vulnerabilities.spec.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5538,4 +5538,69 @@ describe('Vulnerabilities', () => {
55385538
expect(contextAfterDelete.isAdmin).toBeUndefined();
55395539
});
55405540
});
5541+
5542+
describe('(GHSA-hpm8-9qx6-jvwv) Ranged file download bypasses afterFind(Parse.File) trigger and validators', () => {
5543+
it_only_db('mongo')('enforces afterFind requireUser validator on streaming file download', async () => {
5544+
const file = new Parse.File('secret.txt', [1, 2, 3], 'text/plain');
5545+
await file.save({ useMasterKey: true });
5546+
Parse.Cloud.afterFind(Parse.File, () => {}, { requireUser: true });
5547+
const response = await request({
5548+
url: file.url(),
5549+
headers: {
5550+
'X-Parse-Application-Id': 'test',
5551+
'X-Parse-REST-API-Key': 'rest',
5552+
'Range': 'bytes=0-2',
5553+
},
5554+
}).catch(e => e);
5555+
expect(response.status).toBe(403);
5556+
});
5557+
5558+
it('enforces afterFind requireUser validator on non-streaming file download', async () => {
5559+
const file = new Parse.File('secret.txt', [1, 2, 3], 'text/plain');
5560+
await file.save({ useMasterKey: true });
5561+
Parse.Cloud.afterFind(Parse.File, () => {}, { requireUser: true });
5562+
const response = await request({
5563+
url: file.url(),
5564+
headers: {
5565+
'X-Parse-Application-Id': 'test',
5566+
'X-Parse-REST-API-Key': 'rest',
5567+
},
5568+
}).catch(e => e);
5569+
expect(response.status).toBe(403);
5570+
});
5571+
5572+
it_only_db('mongo')('allows streaming file download when afterFind requireUser validator passes', async () => {
5573+
const file = new Parse.File('secret.txt', [1, 2, 3], 'text/plain');
5574+
await file.save({ useMasterKey: true });
5575+
const user = await Parse.User.signUp('username', 'password');
5576+
Parse.Cloud.afterFind(Parse.File, () => {}, { requireUser: true });
5577+
const response = await request({
5578+
url: file.url(),
5579+
headers: {
5580+
'X-Parse-Application-Id': 'test',
5581+
'X-Parse-REST-API-Key': 'rest',
5582+
'X-Parse-Session-Token': user.getSessionToken(),
5583+
'Range': 'bytes=0-2',
5584+
},
5585+
}).catch(e => e);
5586+
expect(response.status).toBe(206);
5587+
});
5588+
5589+
it_only_db('mongo')('enforces afterFind custom authorization on streaming file download', async () => {
5590+
const file = new Parse.File('secret.txt', [1, 2, 3], 'text/plain');
5591+
await file.save({ useMasterKey: true });
5592+
Parse.Cloud.afterFind(Parse.File, () => {
5593+
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Access denied');
5594+
});
5595+
const response = await request({
5596+
url: file.url(),
5597+
headers: {
5598+
'X-Parse-Application-Id': 'test',
5599+
'X-Parse-REST-API-Key': 'rest',
5600+
'Range': 'bytes=0-2',
5601+
},
5602+
}).catch(e => e);
5603+
expect(response.status).toBe(403);
5604+
});
5605+
});
55415606
});

src/Routers/FilesRouter.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Config from '../Config';
55
import logger from '../logger';
66
const triggers = require('../triggers');
77
const Utils = require('../Utils');
8+
const auth = require('../Auth');
89
import { Readable } from 'stream';
910
import { createSanitizedHttpError } from '../Error';
1011

@@ -120,6 +121,22 @@ export class FilesRouter {
120121
return Array.isArray(parts) ? parts.join('/') : parts;
121122
}
122123

124+
static async _resolveAuth(req, config) {
125+
const sessionToken = req.get('X-Parse-Session-Token');
126+
if (!sessionToken) {
127+
return null;
128+
}
129+
try {
130+
return await auth.getAuthForSessionToken({
131+
config,
132+
sessionToken,
133+
installationId: req.get('X-Parse-Installation-Id'),
134+
});
135+
} catch {
136+
return null;
137+
}
138+
}
139+
123140
static validateDirectory(directory) {
124141
if (typeof directory !== 'string') {
125142
return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Directory must be a string.');
@@ -177,11 +194,12 @@ export class FilesRouter {
177194
const mime = (await import('mime')).default;
178195
let contentType = mime.getType(filename);
179196
let file = new Parse.File(filename, { base64: '' }, contentType);
197+
const fileAuth = await FilesRouter._resolveAuth(req, config);
180198
const triggerResult = await triggers.maybeRunFileTrigger(
181199
triggers.Types.beforeFind,
182200
{ file },
183201
config,
184-
req.auth
202+
fileAuth
185203
);
186204
if (triggerResult?.file?._name) {
187205
filename = triggerResult?.file?._name;
@@ -191,6 +209,12 @@ export class FilesRouter {
191209
const defaultResponseHeaders = { 'X-Content-Type-Options': 'nosniff' };
192210

193211
if (isFileStreamable(req, filesController)) {
212+
await triggers.maybeRunFileTrigger(
213+
triggers.Types.afterFind,
214+
{ file, forceDownload: false, responseHeaders: { ...defaultResponseHeaders } },
215+
config,
216+
fileAuth
217+
);
194218
for (const [key, value] of Object.entries(defaultResponseHeaders)) {
195219
res.set(key, value);
196220
}
@@ -215,7 +239,7 @@ export class FilesRouter {
215239
triggers.Types.afterFind,
216240
{ file, forceDownload: false, responseHeaders: { ...defaultResponseHeaders } },
217241
config,
218-
req.auth
242+
fileAuth
219243
);
220244

221245
if (afterFind?.file) {
@@ -736,11 +760,12 @@ export class FilesRouter {
736760
const { filesController } = config;
737761
let filename = FilesRouter._getFilenameFromParams(req);
738762
const file = new Parse.File(filename, { base64: '' });
763+
const fileAuth = await FilesRouter._resolveAuth(req, config);
739764
const triggerResult = await triggers.maybeRunFileTrigger(
740765
triggers.Types.beforeFind,
741766
{ file },
742767
config,
743-
req.auth
768+
fileAuth
744769
);
745770
if (triggerResult?.file?._name) {
746771
filename = triggerResult.file._name;
@@ -756,7 +781,7 @@ export class FilesRouter {
756781
triggers.Types.afterFind,
757782
{ file },
758783
config,
759-
req.auth
784+
fileAuth
760785
);
761786
res.status(200);
762787
res.json(data);

0 commit comments

Comments
 (0)