Skip to content

Commit 81dd350

Browse files
authored
fix: Improve keep extension (#857)
* tests: add failing test case * docs: add comment * tests: add comment * tests: add a test case that proves that the regex was always bad * fix: replace regex with reliable filtering * feat: stop at first invalid char * feat: allow numbers in file extensions * feat: stop extension from being '.' * refactor: code style * docs: use slugify in the example * chore: prepare release
1 parent 971e3a7 commit 81dd350

File tree

5 files changed

+56
-20
lines changed

5 files changed

+56
-20
lines changed

examples/with-http.js

+11-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import http from 'node:http';
2+
import slugify from '@sindresorhus/slugify';
23
import formidable from '../src/index.js';
34

45

@@ -14,16 +15,16 @@ const server = http.createServer((req, res) => {
1415
const form = formidable({
1516
// uploadDir: `uploads`,
1617
keepExtensions: true,
17-
// filename(/*name, ext, part, form*/) {
18-
// /* name basename of the http originalFilename
19-
// ext with the dot ".txt" only if keepExtensions is true
20-
// */
21-
// // slugify to avoid invalid filenames
22-
// // substr to define a maximum length
23-
// // return `${slugify(name)}.${slugify(ext, {separator: ''})}`.substr(0, 100);
24-
// return 'yo.txt'; // or completly different name
25-
// // return 'z/yo.txt'; // subdirectory
26-
// },
18+
filename(name, ext, part, form) {
19+
/* name basename of the http originalFilename
20+
ext with the dot ".txt" only if keepExtensions is true
21+
*/
22+
// slugify to avoid invalid filenames
23+
// substr to define a maximum
24+
return `${slugify(name)}.${slugify(ext, {separator: ''})}`.substr(0, 100);
25+
// return 'yo.txt'; // or completly different name
26+
// return 'z/yo.txt'; // subdirectory
27+
},
2728
// filter: function ({name, originalFilename, mimetype}) {
2829
// // keep only images
2930
// return mimetype && mimetype.includes("image");

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "formidable",
3-
"version": "3.2.3",
3+
"version": "3.2.4",
44
"license": "MIT",
55
"description": "A node.js module for parsing form data, especially file uploads.",
66
"homepage": "https://github.com/node-formidable/formidable",
@@ -37,6 +37,7 @@
3737
"devDependencies": {
3838
"@commitlint/cli": "8.3.5",
3939
"@commitlint/config-conventional": "8.3.4",
40+
"@sindresorhus/slugify": "^2.1.0",
4041
"@tunnckocore/prettier-config": "1.3.8",
4142
"del-cli": "3.0.0",
4243
"eslint": "6.8.0",

src/Formidable.js

+27-4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ function hasOwnProp(obj, key) {
4242
return Object.prototype.hasOwnProperty.call(obj, key);
4343
}
4444

45+
const invalidExtensionChar = (c) => {
46+
const code = c.charCodeAt(0);
47+
return !(
48+
code === 46 || // .
49+
(code >= 48 && code <= 57) ||
50+
(code >= 65 && code <= 90) ||
51+
(code >= 97 && code <= 122)
52+
);
53+
};
54+
4555
class IncomingForm extends EventEmitter {
4656
constructor(options = {}) {
4757
super();
@@ -497,6 +507,9 @@ class IncomingForm extends EventEmitter {
497507
return originalFilename;
498508
}
499509

510+
// able to get composed extension with multiple dots
511+
// "a.b.c" -> ".b.c"
512+
// as opposed to path.extname -> ".c"
500513
_getExtension(str) {
501514
if (!str) {
502515
return '';
@@ -505,13 +518,23 @@ class IncomingForm extends EventEmitter {
505518
const basename = path.basename(str);
506519
const firstDot = basename.indexOf('.');
507520
const lastDot = basename.lastIndexOf('.');
508-
const extname = path.extname(basename).replace(/(\.[a-z0-9]+).*/i, '$1');
521+
let rawExtname = path.extname(basename);
509522

510-
if (firstDot === lastDot) {
511-
return extname;
523+
if (firstDot !== lastDot) {
524+
rawExtname = basename.slice(firstDot);
512525
}
513526

514-
return basename.slice(firstDot, lastDot) + extname;
527+
let filtered;
528+
const firstInvalidIndex = Array.from(rawExtname).findIndex(invalidExtensionChar);
529+
if (firstInvalidIndex === -1) {
530+
filtered = rawExtname;
531+
} else {
532+
filtered = rawExtname.substring(0, firstInvalidIndex);
533+
}
534+
if (filtered === '.') {
535+
return '';
536+
}
537+
return filtered;
515538
}
516539

517540
_joinDirectoryName(name) {

src/PersistentFile.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable no-underscore-dangle */
22

3-
import { WriteStream, unlink } from 'node:fs';
4-
import { createHash } from 'node:crypto';
3+
import fs from 'node:fs';
4+
import crypto from 'node:crypto';
55
import { EventEmitter } from 'node:events';
66

77
class PersistentFile extends EventEmitter {
@@ -15,14 +15,14 @@ class PersistentFile extends EventEmitter {
1515
this._writeStream = null;
1616

1717
if (typeof this.hashAlgorithm === 'string') {
18-
this.hash = createHash(this.hashAlgorithm);
18+
this.hash = crypto.createHash(this.hashAlgorithm);
1919
} else {
2020
this.hash = null;
2121
}
2222
}
2323

2424
open() {
25-
this._writeStream = new WriteStream(this.filepath);
25+
this._writeStream = fs.createWriteStream(this.filepath);
2626
this._writeStream.on('error', (err) => {
2727
this.emit('error', err);
2828
});
@@ -80,7 +80,7 @@ class PersistentFile extends EventEmitter {
8080
this._writeStream.destroy();
8181
const filepath = this.filepath;
8282
setTimeout(function () {
83-
unlink(filepath, () => {});
83+
fs.unlink(filepath, () => {});
8484
}, 1)
8585
}
8686
}

test/unit/formidable.test.js

+11
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ function makeHeader(originalFilename) {
6565

6666
const getBasename = (part) => path.basename(form._getNewName(part));
6767

68+
// tests below assume baseline hexoid 25 chars + a few more for the extension
6869
let basename = getBasename('fine.jpg?foo=bar');
6970
expect(basename).toHaveLength(29);
7071
let ext = path.extname(basename);
@@ -94,6 +95,16 @@ function makeHeader(originalFilename) {
9495
expect(basename).toHaveLength(30);
9596
ext = path.extname(basename);
9697
expect(ext).toBe('.QxZs');
98+
99+
basename = getBasename('test.pdf.jqlnn<img src=a onerror=alert(1)>.png');
100+
expect(basename).toHaveLength(35);
101+
ext = path.extname(basename);
102+
expect(ext).toBe('.jqlnn');
103+
104+
basename = getBasename('test.<a.png');
105+
expect(basename).toHaveLength(25);
106+
ext = path.extname(basename);
107+
expect(ext).toBe('');
97108
});
98109

99110
test(`${name}#_Array parameters support`, () => {

0 commit comments

Comments
 (0)