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 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** β€” For enterprise support and consulting, contact the maintainers
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,