pompelmi 0.32.1 β 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -2
- package/dist/pompelmi.cjs +77 -81
- package/dist/pompelmi.cjs.map +1 -1
- package/dist/pompelmi.esm.js +77 -81
- package/dist/pompelmi.esm.js.map +1 -1
- package/dist/types/scanners/common-heuristics.d.ts +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
<a href="https://www.producthunt.com/products/pompelmi"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1010722&theme=light" alt="pompelmi - Secure File Upload Scanning for Node.js | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
|
36
36
|
<br/>
|
|
37
37
|
<a href="https://www.helpnetsecurity.com/2026/02/02/pompelmi-open-source-secure-file-upload-scanning-node-js/"><img alt="Featured on HelpNet Security" src="https://img.shields.io/badge/π_FEATURED-HelpNet%20Security-FF6B35?style=for-the-badge"></a>
|
|
38
|
+
<a href="https://stackoverflow.blog/2026/02/23/defense-against-uploads-oss-file-scanner-pompelmi/"><img alt="Featured on Stack Overflow Blog" src="https://img.shields.io/badge/π_FEATURED-Stack%20Overflow%20Blog-F58025?style=for-the-badge&logo=stackoverflow&logoColor=white"></a>
|
|
38
39
|
<a href="https://snyk.io/test/github/pompelmi/pompelmi"><img alt="Secured by Snyk" src="https://img.shields.io/badge/π‘οΈ_SECURED_BY-Snyk-4C4A73?style=for-the-badge&logo=snyk"></a>
|
|
39
40
|
<br/>
|
|
40
41
|
<a href="https://github.com/sorrycc/awesome-javascript"><img alt="Mentioned in Awesome JavaScript" src="https://awesome.re/mentioned-badge.svg"></a>
|
|
@@ -181,6 +182,7 @@ npm i pompelmi @pompelmi/express-middleware
|
|
|
181
182
|
- [Security Notes](#-security-notes)
|
|
182
183
|
- [Releases & Security](#-releases--security)
|
|
183
184
|
- [Community & Recognition](#-community--recognition)
|
|
185
|
+
- [Commercial Support](#-commercial-support)
|
|
184
186
|
- [FAQ](#-faq)
|
|
185
187
|
- [Tests & Coverage](#-tests--coverage)
|
|
186
188
|
- [Contributing](#-contributing)
|
|
@@ -1081,7 +1083,7 @@ _Want to share your experience? [Open a discussion](https://github.com/pompelmi/
|
|
|
1081
1083
|
- π¬ **[GitHub Discussions](https://github.com/pompelmi/pompelmi/discussions)** β Ask questions, share ideas, get community support
|
|
1082
1084
|
- π **[Issue Tracker](https://github.com/pompelmi/pompelmi/issues)** β Report bugs, request features
|
|
1083
1085
|
- π **[Security Policy](https://github.com/pompelmi/pompelmi/security)** β Report security vulnerabilities privately
|
|
1084
|
-
- πΌ **Commercial Support** β
|
|
1086
|
+
- πΌ **[Commercial Support](#-commercial-support)** β Private, async support by the maintainer for integration help, troubleshooting, and configuration review
|
|
1085
1087
|
- π **[Sponsor pompelmi](https://github.com/sponsors/pompelmi)** β Support ongoing development via GitHub Sponsors
|
|
1086
1088
|
|
|
1087
1089
|
**Supported Frameworks:**
|
|
@@ -1096,6 +1098,35 @@ _Want to share your experience? [Open a discussion](https://github.com/pompelmi/
|
|
|
1096
1098
|
|
|
1097
1099
|
---
|
|
1098
1100
|
|
|
1101
|
+
## πΌ Commercial Support
|
|
1102
|
+
|
|
1103
|
+
Limited commercial support is available for teams using pompelmi.
|
|
1104
|
+
|
|
1105
|
+
Support is offered on a **private, asynchronous, best-effort basis** by the maintainer and may include:
|
|
1106
|
+
|
|
1107
|
+
- Integration assistance
|
|
1108
|
+
- Configuration review
|
|
1109
|
+
- Prioritized troubleshooting
|
|
1110
|
+
- Upload security guidance
|
|
1111
|
+
|
|
1112
|
+
Support is provided **in writing only**. Live calls and real-time support are not included.
|
|
1113
|
+
|
|
1114
|
+
**To inquire**, email [pompelmideveloper@yahoo.com](mailto:pompelmideveloper@yahoo.com) with the following details:
|
|
1115
|
+
|
|
1116
|
+
- Framework / runtime (e.g. Express, Next.js, Koa)
|
|
1117
|
+
- Node.js version
|
|
1118
|
+
- pompelmi version
|
|
1119
|
+
- A short description of the issue or goal
|
|
1120
|
+
- Expected behavior
|
|
1121
|
+
- Relevant logs or errors β avoid including secrets or sensitive data in your initial message
|
|
1122
|
+
- Urgency
|
|
1123
|
+
- Whether you need integration help, troubleshooting, or a configuration review
|
|
1124
|
+
|
|
1125
|
+
> Community support (GitHub Issues, Discussions, and public docs) remains free and open to everyone.
|
|
1126
|
+
> For private vulnerability disclosure, see [SECURITY.md](./SECURITY.md).
|
|
1127
|
+
|
|
1128
|
+
---
|
|
1129
|
+
|
|
1099
1130
|
## ποΈ Contributors
|
|
1100
1131
|
|
|
1101
1132
|
Thanks to all the amazing contributors who have helped make pompelmi better!
|
|
@@ -1153,9 +1184,12 @@ In the examples, the guard attaches scan data to the request context (e.g. `req.
|
|
|
1153
1184
|
**Why 422 for blocked files?**
|
|
1154
1185
|
Using **422** to signal a policy violation keeps it distinct from transport errors; itβs a common pattern. Use the codes that best match your API guidelines.
|
|
1155
1186
|
|
|
1156
|
-
**Are ZIP bombs handled?**
|
|
1187
|
+
**Are ZIP bombs handled?**
|
|
1157
1188
|
Archives are traversed with limits to reduce archiveβbomb risk. Keep your size limits conservative and prefer `failClosed: true` in production.
|
|
1158
1189
|
|
|
1190
|
+
**Is commercial support available?**
|
|
1191
|
+
Yes. Limited commercial support is available on a private, asynchronous, best-effort basis from the maintainer. Support is in writing only β no live calls or real-time support. Email [pompelmideveloper@yahoo.com](mailto:pompelmideveloper@yahoo.com). See the [Commercial Support](#-commercial-support) section for full details and the inquiry template.
|
|
1192
|
+
|
|
1159
1193
|
---
|
|
1160
1194
|
|
|
1161
1195
|
## π§ͺ Tests & Coverage
|
package/dist/pompelmi.cjs
CHANGED
|
@@ -25,6 +25,81 @@ var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
|
|
|
25
25
|
var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
|
|
26
26
|
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
27
27
|
|
|
28
|
+
function hasAsciiToken(buf, token) {
|
|
29
|
+
// Use latin1 so we can safely search binary
|
|
30
|
+
return buf.indexOf(token, 0, 'latin1') !== -1;
|
|
31
|
+
}
|
|
32
|
+
function startsWith(buf, bytes) {
|
|
33
|
+
if (buf.length < bytes.length)
|
|
34
|
+
return false;
|
|
35
|
+
for (let i = 0; i < bytes.length; i++)
|
|
36
|
+
if (buf[i] !== bytes[i])
|
|
37
|
+
return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
function isPDF(buf) {
|
|
41
|
+
// %PDF-
|
|
42
|
+
return startsWith(buf, [0x25, 0x50, 0x44, 0x46, 0x2d]);
|
|
43
|
+
}
|
|
44
|
+
function isOleCfb(buf) {
|
|
45
|
+
// D0 CF 11 E0 A1 B1 1A E1
|
|
46
|
+
const sig = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
|
|
47
|
+
return startsWith(buf, sig);
|
|
48
|
+
}
|
|
49
|
+
function isZipLike$1(buf) {
|
|
50
|
+
// PK\x03\x04
|
|
51
|
+
return startsWith(buf, [0x50, 0x4b, 0x03, 0x04]);
|
|
52
|
+
}
|
|
53
|
+
function isPeExecutable(buf) {
|
|
54
|
+
// "MZ"
|
|
55
|
+
return startsWith(buf, [0x4d, 0x5a]);
|
|
56
|
+
}
|
|
57
|
+
/** OOXML macro hint via filename token in ZIP container */
|
|
58
|
+
function hasOoxmlMacros(buf) {
|
|
59
|
+
if (!isZipLike$1(buf))
|
|
60
|
+
return false;
|
|
61
|
+
return hasAsciiToken(buf, 'vbaProject.bin');
|
|
62
|
+
}
|
|
63
|
+
/** PDF risky features (/JavaScript, /OpenAction, /AA, /Launch) */
|
|
64
|
+
function pdfRiskTokens(buf) {
|
|
65
|
+
const tokens = ['/JavaScript', '/OpenAction', '/AA', '/Launch'];
|
|
66
|
+
return tokens.filter(t => hasAsciiToken(buf, t));
|
|
67
|
+
}
|
|
68
|
+
const CommonHeuristicsScanner = {
|
|
69
|
+
async scan(input) {
|
|
70
|
+
const buf = Buffer.from(input);
|
|
71
|
+
const matches = [];
|
|
72
|
+
// Office macros (OLE / OOXML)
|
|
73
|
+
if (isOleCfb(buf)) {
|
|
74
|
+
matches.push({ rule: 'office_ole_container', severity: 'suspicious' });
|
|
75
|
+
}
|
|
76
|
+
if (hasOoxmlMacros(buf)) {
|
|
77
|
+
matches.push({ rule: 'office_ooxml_macros', severity: 'suspicious' });
|
|
78
|
+
}
|
|
79
|
+
// PDF risky tokens
|
|
80
|
+
if (isPDF(buf)) {
|
|
81
|
+
const toks = pdfRiskTokens(buf);
|
|
82
|
+
if (toks.length) {
|
|
83
|
+
matches.push({
|
|
84
|
+
rule: 'pdf_risky_actions',
|
|
85
|
+
severity: 'suspicious',
|
|
86
|
+
meta: { tokens: toks }
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Executable header
|
|
91
|
+
if (isPeExecutable(buf)) {
|
|
92
|
+
matches.push({ rule: 'pe_executable_signature', severity: 'suspicious' });
|
|
93
|
+
}
|
|
94
|
+
// EICAR test file
|
|
95
|
+
const EICAR_NEEDLE = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!";
|
|
96
|
+
if (hasAsciiToken(buf, EICAR_NEEDLE)) {
|
|
97
|
+
matches.push({ rule: 'eicar_test_file', severity: 'high', meta: { note: 'EICAR standard antivirus test file detected' } });
|
|
98
|
+
}
|
|
99
|
+
return matches;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
28
103
|
function toScanFn(s) {
|
|
29
104
|
return (typeof s === "function" ? s : s.scan);
|
|
30
105
|
}
|
|
@@ -135,6 +210,8 @@ function composeScanners(...args) {
|
|
|
135
210
|
}
|
|
136
211
|
function createPresetScanner(preset, opts = {}) {
|
|
137
212
|
const scanners = [];
|
|
213
|
+
// Always include heuristics (EICAR, PHP webshells, JS obfuscation, PE hints, etc.)
|
|
214
|
+
scanners.push(CommonHeuristicsScanner);
|
|
138
215
|
// Add decompilation scanners based on preset
|
|
139
216
|
if (preset === 'decompilation-basic' || preset === 'decompilation-deep' ||
|
|
140
217
|
preset === 'malware-analysis' || opts.enableDecompilation) {
|
|
@@ -182,17 +259,6 @@ function createPresetScanner(preset, opts = {}) {
|
|
|
182
259
|
}
|
|
183
260
|
}
|
|
184
261
|
}
|
|
185
|
-
// Add other scanners for advanced presets
|
|
186
|
-
if (preset === 'advanced' || preset === 'malware-analysis') {
|
|
187
|
-
// Add heuristics scanner
|
|
188
|
-
try {
|
|
189
|
-
const { CommonHeuristicsScanner } = require('./scanners/common-heuristics');
|
|
190
|
-
scanners.push(new CommonHeuristicsScanner());
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
// Heuristics not available
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
262
|
if (scanners.length === 0) {
|
|
197
263
|
// Fallback scanner that returns no matches
|
|
198
264
|
return async (_input, _ctx) => {
|
|
@@ -3143,76 +3209,6 @@ function mapMatchesToVerdict(matches = []) {
|
|
|
3143
3209
|
return isMal ? 'malicious' : 'suspicious';
|
|
3144
3210
|
}
|
|
3145
3211
|
|
|
3146
|
-
function hasAsciiToken(buf, token) {
|
|
3147
|
-
// Use latin1 so we can safely search binary
|
|
3148
|
-
return buf.indexOf(token, 0, 'latin1') !== -1;
|
|
3149
|
-
}
|
|
3150
|
-
function startsWith(buf, bytes) {
|
|
3151
|
-
if (buf.length < bytes.length)
|
|
3152
|
-
return false;
|
|
3153
|
-
for (let i = 0; i < bytes.length; i++)
|
|
3154
|
-
if (buf[i] !== bytes[i])
|
|
3155
|
-
return false;
|
|
3156
|
-
return true;
|
|
3157
|
-
}
|
|
3158
|
-
function isPDF(buf) {
|
|
3159
|
-
// %PDF-
|
|
3160
|
-
return startsWith(buf, [0x25, 0x50, 0x44, 0x46, 0x2d]);
|
|
3161
|
-
}
|
|
3162
|
-
function isOleCfb(buf) {
|
|
3163
|
-
// D0 CF 11 E0 A1 B1 1A E1
|
|
3164
|
-
const sig = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
|
|
3165
|
-
return startsWith(buf, sig);
|
|
3166
|
-
}
|
|
3167
|
-
function isZipLike$1(buf) {
|
|
3168
|
-
// PK\x03\x04
|
|
3169
|
-
return startsWith(buf, [0x50, 0x4b, 0x03, 0x04]);
|
|
3170
|
-
}
|
|
3171
|
-
function isPeExecutable(buf) {
|
|
3172
|
-
// "MZ"
|
|
3173
|
-
return startsWith(buf, [0x4d, 0x5a]);
|
|
3174
|
-
}
|
|
3175
|
-
/** OOXML macro hint via filename token in ZIP container */
|
|
3176
|
-
function hasOoxmlMacros(buf) {
|
|
3177
|
-
if (!isZipLike$1(buf))
|
|
3178
|
-
return false;
|
|
3179
|
-
return hasAsciiToken(buf, 'vbaProject.bin');
|
|
3180
|
-
}
|
|
3181
|
-
/** PDF risky features (/JavaScript, /OpenAction, /AA, /Launch) */
|
|
3182
|
-
function pdfRiskTokens(buf) {
|
|
3183
|
-
const tokens = ['/JavaScript', '/OpenAction', '/AA', '/Launch'];
|
|
3184
|
-
return tokens.filter(t => hasAsciiToken(buf, t));
|
|
3185
|
-
}
|
|
3186
|
-
const CommonHeuristicsScanner = {
|
|
3187
|
-
async scan(input) {
|
|
3188
|
-
const buf = Buffer.from(input);
|
|
3189
|
-
const matches = [];
|
|
3190
|
-
// Office macros (OLE / OOXML)
|
|
3191
|
-
if (isOleCfb(buf)) {
|
|
3192
|
-
matches.push({ rule: 'office_ole_container', severity: 'suspicious' });
|
|
3193
|
-
}
|
|
3194
|
-
if (hasOoxmlMacros(buf)) {
|
|
3195
|
-
matches.push({ rule: 'office_ooxml_macros', severity: 'suspicious' });
|
|
3196
|
-
}
|
|
3197
|
-
// PDF risky tokens
|
|
3198
|
-
if (isPDF(buf)) {
|
|
3199
|
-
const toks = pdfRiskTokens(buf);
|
|
3200
|
-
if (toks.length) {
|
|
3201
|
-
matches.push({
|
|
3202
|
-
rule: 'pdf_risky_actions',
|
|
3203
|
-
severity: 'suspicious',
|
|
3204
|
-
meta: { tokens: toks }
|
|
3205
|
-
});
|
|
3206
|
-
}
|
|
3207
|
-
}
|
|
3208
|
-
// Executable header
|
|
3209
|
-
if (isPeExecutable(buf)) {
|
|
3210
|
-
matches.push({ rule: 'pe_executable_signature', severity: 'suspicious' });
|
|
3211
|
-
}
|
|
3212
|
-
return matches;
|
|
3213
|
-
}
|
|
3214
|
-
};
|
|
3215
|
-
|
|
3216
3212
|
const SIG_CEN = 0x02014b50;
|
|
3217
3213
|
const DEFAULTS = {
|
|
3218
3214
|
maxEntries: 1000,
|