Skip to content

Commit

Permalink
throw a descriptive error for too-long encoded filenames (#874)
Browse files Browse the repository at this point in the history
  • Loading branch information
kreddlear authored Oct 3, 2024
1 parent 9263605 commit 16a24a6
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 22 deletions.
3 changes: 3 additions & 0 deletions packages/core/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const RESPONSE_SIZE_LIMIT = 6291456;
const UPLOAD_MAX_SIZE = 1000 * 1000 * 1000 * 1; // 1GB, in zapier backend too
const NON_STREAM_UPLOAD_MAX_SIZE = 1000 * 1000 * 150;

const ENCODED_FILENAME_MAX_LENGTH = 1000; // 1KB - S3 Metadata max is 2048

const HYDRATE_DIRECTIVE_HOIST = '$HOIST$';

const RENDER_ONLY_METHODS = [
Expand Down Expand Up @@ -58,6 +60,7 @@ const PACKAGE_VERSION = packageJson.version;
module.exports = {
DEFAULT_LOGGING_HTTP_API_KEY,
DEFAULT_LOGGING_HTTP_ENDPOINT,
ENCODED_FILENAME_MAX_LENGTH,
HYDRATE_DIRECTIVE_HOIST,
IS_TESTING,
KILL_MAX_LIMIT,
Expand Down
26 changes: 25 additions & 1 deletion packages/core/src/tools/create-file-stasher.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ const contentDisposition = require('content-disposition');

const mime = require('mime-types');

const { UPLOAD_MAX_SIZE, NON_STREAM_UPLOAD_MAX_SIZE } = require('../constants');
const {
ENCODED_FILENAME_MAX_LENGTH,
UPLOAD_MAX_SIZE,
NON_STREAM_UPLOAD_MAX_SIZE,
} = require('../constants');
const uploader = require('./uploader');

const DEFAULT_FILE_NAME = 'unnamedfile';
Expand Down Expand Up @@ -186,6 +190,24 @@ const ensureUploadMaxSizeNotExceeded = (streamOrData, length) => {
}
};

// S3's max metadata size is 2KB
// If the filename needs to be encoded, both the filename
// and encoded filename are included in the Content-Disposition header
const ensureMetadataMaxSizeNotExceeded = (filename) => {
const filenameMaxSize = ENCODED_FILENAME_MAX_LENGTH;
if (filename) {
const encodedFilename = encodeURIComponent(filename);
if (
encodedFilename !== filename &&
encodedFilename.length > filenameMaxSize
) {
throw new Error(
`URI-Encoded Filename is too long at ${encodedFilename.length}, ${ENCODED_FILENAME_MAX_LENGTH} is the max.`
);
}
}
};

// Designed to be some user provided function/api.
const createFileStasher = (input) => {
const rpc = _.get(input, '_zapier.rpc');
Expand Down Expand Up @@ -247,6 +269,8 @@ const createFileStasher = (input) => {
const finalLength = knownLength || length;
ensureUploadMaxSizeNotExceeded(streamOrData, finalLength);

ensureMetadataMaxSizeNotExceeded(filename || _filename);

return uploader(
signedPostData,
streamOrData,
Expand Down
63 changes: 42 additions & 21 deletions packages/core/test/tools/file-stasher.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ const {
const createFileStasher = require('../../src/tools/create-file-stasher');
const createAppRequestClient = require('../../src/tools/create-app-request-client');
const createInput = require('../../src/tools/create-input');
const { UPLOAD_MAX_SIZE, NON_STREAM_UPLOAD_MAX_SIZE } = require('../../src/constants');
const {
UPLOAD_MAX_SIZE,
NON_STREAM_UPLOAD_MAX_SIZE,
ENCODED_FILENAME_MAX_LENGTH,
} = require('../../src/constants');

const sha1 = (stream) =>
new Promise((resolve, reject) => {
Expand Down Expand Up @@ -178,6 +182,23 @@ describe('file upload', () => {
);
});

it('should fail a file with a too-long encoded text filename', async () => {
mockRpcGetPresignedPostCall('8888/new.txt');
mockUpload();

const file = Buffer.from('hello world this is a buffer of text');
// length 1026
const filename =
'太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了太長了';
const encodedLength = encodeURIComponent(filename).length;

const knownLength = Buffer.byteLength(file);

await stashFile(file, knownLength, filename).should.be.rejectedWith(
`URI-Encoded Filename is too long at ${encodedLength}, ${ENCODED_FILENAME_MAX_LENGTH} is the max.`
);
});

it('should throwForStatus if bad status', async () => {
mockRpcGetPresignedPostCall('4444/deadbeef');
mockUpload();
Expand Down Expand Up @@ -295,26 +316,26 @@ describe('file upload', () => {
});

it('should handle bad content-disposition', async () => {
mockRpcGetPresignedPostCall('1234/foo.json');
mockUpload();

const file = request({
url: 'https://httpbin.zapier-tooling.com/response-headers',
params: {
// Missing a closing quote at the end
'Content-Disposition': 'inline; filename="an example.json',
},
raw: true,
});
const url = await stashFile(file);
should(url).eql(`${FAKE_S3_URL}/1234/foo.json`);

const s3Response = await request({ url, raw: true });
should(s3Response.getHeader('content-type')).startWith('application/json');
should(s3Response.getHeader('content-disposition')).eql(
'attachment; filename="response-headers.json"'
);
});
mockRpcGetPresignedPostCall('1234/foo.json');
mockUpload();

const file = request({
url: 'https://httpbin.zapier-tooling.com/response-headers',
params: {
// Missing a closing quote at the end
'Content-Disposition': 'inline; filename="an example.json',
},
raw: true,
});
const url = await stashFile(file);
should(url).eql(`${FAKE_S3_URL}/1234/foo.json`);

const s3Response = await request({ url, raw: true });
should(s3Response.getHeader('content-type')).startWith('application/json');
should(s3Response.getHeader('content-disposition')).eql(
'attachment; filename="response-headers.json"'
);
});

it('should upload a png image', async () => {
mockRpcGetPresignedPostCall('1234/pig.png');
Expand Down

0 comments on commit 16a24a6

Please sign in to comment.