@modern-admin/feature-upload 0.2.1 → 0.3.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/dist/graphql.d.ts.map +1 -1
  3. package/dist/graphql.js +23 -5
  4. package/dist/graphql.js.map +1 -1
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +4 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/mime.d.ts +21 -0
  10. package/dist/mime.d.ts.map +1 -0
  11. package/dist/mime.js +46 -0
  12. package/dist/mime.js.map +1 -0
  13. package/dist/nest/upload-sweeper.service.d.ts.map +1 -1
  14. package/dist/nest/upload-sweeper.service.js +7 -0
  15. package/dist/nest/upload-sweeper.service.js.map +1 -1
  16. package/dist/nest/upload.controller.d.ts.map +1 -1
  17. package/dist/nest/upload.controller.js +82 -15
  18. package/dist/nest/upload.controller.js.map +1 -1
  19. package/dist/nest/upload.module.d.ts +5 -0
  20. package/dist/nest/upload.module.d.ts.map +1 -1
  21. package/dist/nest/upload.module.js +5 -0
  22. package/dist/nest/upload.module.js.map +1 -1
  23. package/dist/nest/upload.tokens.d.ts +23 -0
  24. package/dist/nest/upload.tokens.d.ts.map +1 -1
  25. package/dist/path-safety.d.ts +45 -0
  26. package/dist/path-safety.d.ts.map +1 -0
  27. package/dist/path-safety.js +73 -0
  28. package/dist/path-safety.js.map +1 -0
  29. package/dist/pending-registry.d.ts +19 -4
  30. package/dist/pending-registry.d.ts.map +1 -1
  31. package/dist/pending-registry.js +19 -4
  32. package/dist/pending-registry.js.map +1 -1
  33. package/dist/providers/local.d.ts.map +1 -1
  34. package/dist/providers/local.js +16 -3
  35. package/dist/providers/local.js.map +1 -1
  36. package/dist/providers/s3.d.ts.map +1 -1
  37. package/dist/providers/s3.js +14 -0
  38. package/dist/providers/s3.js.map +1 -1
  39. package/dist/registry.d.ts +8 -0
  40. package/dist/registry.d.ts.map +1 -1
  41. package/dist/registry.js.map +1 -1
  42. package/dist/types.d.ts +10 -2
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/upload-feature.d.ts.map +1 -1
  45. package/dist/upload-feature.js +30 -31
  46. package/dist/upload-feature.js.map +1 -1
  47. package/package.json +8 -7
  48. package/src/graphql.ts +22 -5
  49. package/src/index.ts +5 -0
  50. package/src/mime.ts +42 -0
  51. package/src/nest/upload-sweeper.service.ts +9 -0
  52. package/src/nest/upload.controller.ts +95 -14
  53. package/src/nest/upload.module.ts +5 -0
  54. package/src/nest/upload.tokens.ts +23 -0
  55. package/src/path-safety.ts +73 -0
  56. package/src/pending-registry.ts +19 -4
  57. package/src/providers/local.ts +15 -3
  58. package/src/providers/s3.ts +17 -0
  59. package/src/registry.ts +8 -0
  60. package/src/types.ts +10 -2
  61. package/src/upload-feature.ts +31 -36
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Path-safety helpers shared by the upload providers and the upload
3
+ * entry points (REST controller + GraphQL extension).
4
+ *
5
+ * Storage keys and filenames are attacker-controllable in two ways:
6
+ *
7
+ * 1. **Upload** — the client-supplied `originalName` flows into key
8
+ * generation (default `uuid + extname`, or a custom `uploadPath`). A raw
9
+ * name like `../../evil.sh` must never be able to steer the key outside
10
+ * the intended storage space.
11
+ * 2. **Delete** — a `type: 'file'` property is just a string in the DB. An
12
+ * attacker who can edit a record can write `../../../../etc/passwd` into
13
+ * it; the next edit/delete would call `provider.delete(key)` with that
14
+ * value. Without containment the local provider's `unlink` escapes the
15
+ * upload directory.
16
+ *
17
+ * Defense is layered: keys are rejected at the transport boundary
18
+ * (`isUnsafeKey`), filenames are sanitised before they can influence a key
19
+ * (`sanitizeFilename`), and the local provider resolves every path against its
20
+ * upload directory (`resolveWithinDir`) as the authoritative backstop.
21
+ */
22
+ import { resolve, sep } from 'node:path';
23
+ /**
24
+ * True when `key` must be refused: empty, containing a NUL byte, absolute
25
+ * (POSIX `/…` or Windows `C:\…`), or containing a `..` traversal segment.
26
+ *
27
+ * Forward and back slashes are both treated as separators so a key crafted on
28
+ * one platform cannot escape on another. Legitimate nested keys
29
+ * (`avatars/2024/uuid.jpg`) and prefixes are allowed — only traversal and
30
+ * absolute paths are rejected.
31
+ */
32
+ export function isUnsafeKey(key) {
33
+ if (typeof key !== 'string' || key.length === 0)
34
+ return true;
35
+ if (key.includes('\0'))
36
+ return true;
37
+ const normalized = key.replace(/\\/g, '/');
38
+ if (normalized.startsWith('/'))
39
+ return true; // POSIX absolute
40
+ if (/^[a-zA-Z]:/.test(normalized))
41
+ return true; // Windows drive-absolute
42
+ return normalized.split('/').some((segment) => segment === '..');
43
+ }
44
+ /**
45
+ * Reduce a client-supplied filename to a safe basename that cannot influence
46
+ * the directory a key resolves into. Strips any directory components (either
47
+ * separator), NUL bytes, and leading dots (so `..` / hidden-traversal names
48
+ * collapse). Falls back to `'upload'` when nothing safe remains.
49
+ */
50
+ export function sanitizeFilename(name) {
51
+ if (typeof name !== 'string' || name.length === 0)
52
+ return 'upload';
53
+ const base = name.replace(/\\/g, '/').split('/').pop() ?? '';
54
+ const cleaned = base
55
+ .replace(/\0/g, '')
56
+ .replace(/^\.+/, '') // no leading dots → no bare '..' / hidden traversal
57
+ .trim();
58
+ return cleaned.length > 0 ? cleaned : 'upload';
59
+ }
60
+ /**
61
+ * Resolve `key` against `baseDir` and guarantee the result stays inside it.
62
+ * Throws when the resolved path escapes the directory (traversal / absolute
63
+ * key). Returns the absolute, contained path for the caller to read/write.
64
+ */
65
+ export function resolveWithinDir(baseDir, key) {
66
+ const base = resolve(baseDir);
67
+ const full = resolve(base, key);
68
+ if (full !== base && !full.startsWith(base + sep)) {
69
+ throw new Error(`[modern-admin/feature-upload] refusing to access "${key}" outside the upload directory`);
70
+ }
71
+ return full;
72
+ }
73
+ //# sourceMappingURL=path-safety.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-safety.js","sourceRoot":"","sources":["../src/path-safety.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAExC;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC5D,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IACnC,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAC1C,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA,CAAC,iBAAiB;IAC7D,IAAI,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA,CAAC,yBAAyB;IACxE,OAAO,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,KAAK,IAAI,CAAC,CAAA;AAClE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAa;IAC5C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAA;IAClE,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAA;IAC5D,MAAM,OAAO,GAAG,IAAI;SACjB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;SAClB,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,oDAAoD;SACxE,IAAI,EAAE,CAAA;IACT,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAA;AAChD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,GAAW;IAC3D,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IAC/B,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CACb,qDAAqD,GAAG,gCAAgC,CACzF,CAAA;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -27,13 +27,28 @@
27
27
  * The registry is a process-level singleton (same pattern as
28
28
  * `UploadProviderRegistry`).
29
29
  *
30
+ * ⚠️ SINGLE-INSTANCE ONLY (until a shared store lands).
31
+ * ------------------------------------------------------
32
+ * This `Map` lives in one process's memory. Behind a load balancer with ≥2
33
+ * replicas it is UNSAFE and can lose data:
34
+ *
35
+ * 1. Replica A handles `POST /upload` → `track(key)` in A's map only.
36
+ * 2. Replica B handles the form save → `confirm(key)` is a no-op (B never
37
+ * saw the pending entry).
38
+ * 3. A's sweeper still considers the key pending; once the TTL elapses it
39
+ * DELETES the file the record now references → broken/missing upload.
40
+ *
41
+ * The provider *registry* key is deterministic (`up_<resourceId>_<prop>`), so
42
+ * uploads still ROUTE correctly across replicas — but the pending lifecycle
43
+ * above does not. Run the upload feature single-instance, OR set a large
44
+ * `pendingTtlMs` to widen the confirm window, until the roadmap item below
45
+ * replaces this with a shared store.
46
+ *
30
47
  * TODO(roadmap): swap the in-process `Map` for a Redis-backed store and move
31
48
  * the sweeper driver (`UploadSweeperService`) onto BullMQ. This is required
32
49
  * for multi-instance deployments where one Nest replica serves the upload
33
- * request and a different replica processes the form submission without a
34
- * shared store the second replica would not see the pending entry, the
35
- * confirm hook would no-op, and the file would be swept on the originator.
36
- * BullMQ also gives us crash-safe scheduling and retries.
50
+ * request and a different replica processes the form submission. BullMQ also
51
+ * gives us crash-safe scheduling and retries.
37
52
  */
38
53
  export declare const PendingUploadsRegistry: {
39
54
  /** Mark `key` as a freshly-uploaded, unconfirmed file. */
@@ -1 +1 @@
1
- {"version":3,"file":"pending-registry.d.ts","sourceRoot":"","sources":["../src/pending-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAaH,eAAO,MAAM,sBAAsB;IACjC,0DAA0D;eAC/C,MAAM,cAAc,MAAM,SAAS,MAAM,GAAG,IAAI;IAI3D,iFAAiF;aACxE,MAAM,GAAG,OAAO;IAIzB,2EAA2E;kBAC7D,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI;IAI1C;;;OAGG;gBACe,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc3C;;;OAGG;gBACc,MAAM,GAAgB,OAAO,CAAC,MAAM,CAAC;IAkBtD,wBAAwB;aACf,IAAI;IAIb,uEAAuE;YAC/D,MAAM;CAGf,CAAA"}
1
+ {"version":3,"file":"pending-registry.d.ts","sourceRoot":"","sources":["../src/pending-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAaH,eAAO,MAAM,sBAAsB;IACjC,0DAA0D;eAC/C,MAAM,cAAc,MAAM,SAAS,MAAM,GAAG,IAAI;IAI3D,iFAAiF;aACxE,MAAM,GAAG,OAAO;IAIzB,2EAA2E;kBAC7D,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI;IAI1C;;;OAGG;gBACe,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc3C;;;OAGG;gBACc,MAAM,GAAgB,OAAO,CAAC,MAAM,CAAC;IAkBtD,wBAAwB;aACf,IAAI;IAIb,uEAAuE;YAC/D,MAAM;CAGf,CAAA"}
@@ -27,13 +27,28 @@
27
27
  * The registry is a process-level singleton (same pattern as
28
28
  * `UploadProviderRegistry`).
29
29
  *
30
+ * ⚠️ SINGLE-INSTANCE ONLY (until a shared store lands).
31
+ * ------------------------------------------------------
32
+ * This `Map` lives in one process's memory. Behind a load balancer with ≥2
33
+ * replicas it is UNSAFE and can lose data:
34
+ *
35
+ * 1. Replica A handles `POST /upload` → `track(key)` in A's map only.
36
+ * 2. Replica B handles the form save → `confirm(key)` is a no-op (B never
37
+ * saw the pending entry).
38
+ * 3. A's sweeper still considers the key pending; once the TTL elapses it
39
+ * DELETES the file the record now references → broken/missing upload.
40
+ *
41
+ * The provider *registry* key is deterministic (`up_<resourceId>_<prop>`), so
42
+ * uploads still ROUTE correctly across replicas — but the pending lifecycle
43
+ * above does not. Run the upload feature single-instance, OR set a large
44
+ * `pendingTtlMs` to widen the confirm window, until the roadmap item below
45
+ * replaces this with a shared store.
46
+ *
30
47
  * TODO(roadmap): swap the in-process `Map` for a Redis-backed store and move
31
48
  * the sweeper driver (`UploadSweeperService`) onto BullMQ. This is required
32
49
  * for multi-instance deployments where one Nest replica serves the upload
33
- * request and a different replica processes the form submission without a
34
- * shared store the second replica would not see the pending entry, the
35
- * confirm hook would no-op, and the file would be swept on the originator.
36
- * BullMQ also gives us crash-safe scheduling and retries.
50
+ * request and a different replica processes the form submission. BullMQ also
51
+ * gives us crash-safe scheduling and retries.
37
52
  */
38
53
  import { UploadProviderRegistry } from './registry.js';
39
54
  const _pending = new Map();
@@ -1 +1 @@
1
- {"version":3,"file":"pending-registry.js","sourceRoot":"","sources":["../src/pending-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAStD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAA;AAEhD,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,0DAA0D;IAC1D,KAAK,CAAC,GAAW,EAAE,UAAkB,EAAE,KAAa;QAClD,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,CAAA;IAClE,CAAC;IAED,iFAAiF;IACjF,GAAG,CAAC,GAAW;QACb,OAAO,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAED,2EAA2E;IAC3E,OAAO,CAAC,IAA2B;QACjC,KAAK,MAAM,CAAC,IAAI,IAAI;YAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC1C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC/B,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACpB,MAAM,GAAG,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;QACxD,IAAI,CAAC,GAAG;YAAE,OAAO,KAAK,CAAA;QACtB,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,mCAAmC;QACrC,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;QAClC,IAAI,KAAK,GAAG,CAAC,CAAA;QACb,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,QAAQ,EAAE,CAAC;YACpC,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC;gBAC3B,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;gBACpB,MAAM,GAAG,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;gBACxD,IAAI,CAAC,GAAG;oBAAE,SAAQ;gBAClB,IAAI,CAAC;oBACH,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;oBAC9B,KAAK,EAAE,CAAA;gBACT,CAAC;gBAAC,MAAM,CAAC;oBACP,aAAa;gBACf,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,wBAAwB;IACxB,KAAK;QACH,QAAQ,CAAC,KAAK,EAAE,CAAA;IAClB,CAAC;IAED,uEAAuE;IACvE,IAAI;QACF,OAAO,QAAQ,CAAC,IAAI,CAAA;IACtB,CAAC;CACF,CAAA"}
1
+ {"version":3,"file":"pending-registry.js","sourceRoot":"","sources":["../src/pending-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAStD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAA;AAEhD,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,0DAA0D;IAC1D,KAAK,CAAC,GAAW,EAAE,UAAkB,EAAE,KAAa;QAClD,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,CAAA;IAClE,CAAC;IAED,iFAAiF;IACjF,GAAG,CAAC,GAAW;QACb,OAAO,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAED,2EAA2E;IAC3E,OAAO,CAAC,IAA2B;QACjC,KAAK,MAAM,CAAC,IAAI,IAAI;YAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC1C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC/B,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACpB,MAAM,GAAG,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;QACxD,IAAI,CAAC,GAAG;YAAE,OAAO,KAAK,CAAA;QACtB,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,mCAAmC;QACrC,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;QAClC,IAAI,KAAK,GAAG,CAAC,CAAA;QACb,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,QAAQ,EAAE,CAAC;YACpC,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC;gBAC3B,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;gBACpB,MAAM,GAAG,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;gBACxD,IAAI,CAAC,GAAG;oBAAE,SAAQ;gBAClB,IAAI,CAAC;oBACH,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;oBAC9B,KAAK,EAAE,CAAA;gBACT,CAAC;gBAAC,MAAM,CAAC;oBACP,aAAa;gBACf,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,wBAAwB;IACxB,KAAK;QACH,QAAQ,CAAC,KAAK,EAAE,CAAA;IAClB,CAAC;IAED,uEAAuE;IACvE,IAAI;QACF,OAAO,QAAQ,CAAC,IAAI,CAAA;IACtB,CAAC;CACF,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/providers/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAEhE,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,qBAAa,mBAAoB,YAAW,eAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAElD,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAS/D,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAKrB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQxC,WAAW,IAAI,MAAM;CAItB"}
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/providers/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAEhE,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,qBAAa,mBAAoB,YAAW,eAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAElD,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAW/D,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAKrB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBxC,WAAW,IAAI,MAAM;CAItB"}
@@ -9,15 +9,18 @@
9
9
  * new LocalUploadProvider({ uploadDir: './public/uploads', baseUrl: '/uploads' })
10
10
  */
11
11
  import { mkdir, writeFile, unlink } from 'node:fs/promises';
12
- import { join, extname, dirname } from 'node:path';
12
+ import { extname, dirname } from 'node:path';
13
13
  import { uuidv7 } from '@modern-admin/core';
14
+ import { resolveWithinDir } from '../path-safety.js';
14
15
  export class LocalUploadProvider {
15
16
  constructor(options) {
16
17
  this.options = options;
17
18
  }
18
19
  async upload(file, key) {
19
20
  const resolvedKey = key ?? `${uuidv7()}${extname(file.originalName)}`;
20
- const dest = join(this.options.uploadDir, resolvedKey);
21
+ // Contain the key inside uploadDir — refuses traversal/absolute keys
22
+ // (e.g. a malicious `uploadPath` or `../../evil.sh`).
23
+ const dest = resolveWithinDir(this.options.uploadDir, resolvedKey);
21
24
  // Create the full directory tree (handles nested keys like 'avatars/2024/01/uuid.jpg').
22
25
  await mkdir(dirname(dest), { recursive: true });
23
26
  await writeFile(dest, file.buffer);
@@ -28,8 +31,18 @@ export class LocalUploadProvider {
28
31
  return `${base}/${key}`;
29
32
  }
30
33
  async delete(key) {
34
+ let dest;
31
35
  try {
32
- await unlink(join(this.options.uploadDir, key));
36
+ // Refuse to unlink anything outside uploadDir — a `type: 'file'` value
37
+ // is an arbitrary DB string, so `key` may be attacker-controlled
38
+ // (`../../../../app/src/main.ts`). Never delete outside the directory.
39
+ dest = resolveWithinDir(this.options.uploadDir, key);
40
+ }
41
+ catch {
42
+ return;
43
+ }
44
+ try {
45
+ await unlink(dest);
33
46
  }
34
47
  catch (err) {
35
48
  if (err.code !== 'ENOENT')
@@ -1 +1 @@
1
- {"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/providers/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAoB3C,MAAM,OAAO,mBAAmB;IAC9B,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAE5D,KAAK,CAAC,MAAM,CAAC,IAAkB,EAAE,GAAY;QAC3C,MAAM,WAAW,GAAG,GAAG,IAAI,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAA;QACrE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAA;QACtD,wFAAwF;QACxF,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAC/C,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;QAClC,OAAO,WAAW,CAAA;IACpB,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,UAAU,CAAA;QAC/C,OAAO,GAAG,IAAI,IAAI,GAAG,EAAE,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAA;QACjD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,GAAG,CAAA;QACjE,CAAC;IACH,CAAC;IAED,WAAW;QACT,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,UAAU,CAAA;QAC/C,OAAO,GAAG,IAAI,QAAQ,CAAA;IACxB,CAAC;CACF"}
1
+ {"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/providers/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAoBpD,MAAM,OAAO,mBAAmB;IAC9B,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAE5D,KAAK,CAAC,MAAM,CAAC,IAAkB,EAAE,GAAY;QAC3C,MAAM,WAAW,GAAG,GAAG,IAAI,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAA;QACrE,qEAAqE;QACrE,sDAAsD;QACtD,MAAM,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAA;QAClE,wFAAwF;QACxF,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAC/C,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;QAClC,OAAO,WAAW,CAAA;IACpB,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,UAAU,CAAA;QAC/C,OAAO,GAAG,IAAI,IAAI,GAAG,EAAE,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,IAAI,IAAY,CAAA;QAChB,IAAI,CAAC;YACH,uEAAuE;YACvE,iEAAiE;YACjE,uEAAuE;YACvE,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;QACtD,CAAC;QAAC,MAAM,CAAC;YACP,OAAM;QACR,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAA;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,GAAG,CAAA;QACjE,CAAC;IACH,CAAC;IAED,WAAW;QACT,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,UAAU,CAAA;QAC/C,OAAO,GAAG,IAAI,QAAQ,CAAA;IACxB,CAAC;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAEhE,MAAM,WAAW,eAAe;IAC9B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC1C;AAED,qBAAa,gBAAiB,YAAW,eAAe;IAKpD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAIxB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;IAPlC,OAAO,CAAC,OAAO,CAAY;gBAGR,OAAO,EAAE,eAAe,EAIxB,cAAc,CAAC,EAAE,GAAG,YAAA;YAIzB,MAAM;IA4Bd,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA2CzD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOpC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAaxC;;;;OAIG;IACH,WAAW,IAAI,MAAM,GAAG,SAAS;YAKnB,SAAS;IA0BvB,OAAO,CAAC,aAAa;CAKtB"}
1
+ {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAEhE,MAAM,WAAW,eAAe;IAC9B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC1C;AAED,qBAAa,gBAAiB,YAAW,eAAe;IAKpD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAIxB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;IAPlC,OAAO,CAAC,OAAO,CAAY;gBAGR,OAAO,EAAE,eAAe,EAIxB,cAAc,CAAC,EAAE,GAAG,YAAA;YAIzB,MAAM;IA4Bd,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmDzD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAYpC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBxC;;;;OAIG;IACH,WAAW,IAAI,MAAM,GAAG,SAAS;YAKnB,SAAS;IA0BvB,OAAO,CAAC,aAAa;CAKtB"}
@@ -27,6 +27,7 @@
27
27
  */
28
28
  import { extname } from 'node:path';
29
29
  import { uuidv7 } from '@modern-admin/core';
30
+ import { isUnsafeKey } from '../path-safety.js';
30
31
  export class S3UploadProvider {
31
32
  constructor(options,
32
33
  // Accept a pre-configured S3Client instance (share across providers /
@@ -65,6 +66,12 @@ export class S3UploadProvider {
65
66
  return this._client;
66
67
  }
67
68
  async upload(file, key) {
69
+ // Reject traversal / absolute keys before they reach the bucket. Keys
70
+ // legitimately contain `/` separators (prefixes), but never `..` or a
71
+ // leading slash.
72
+ if (key !== undefined && isUnsafeKey(key)) {
73
+ throw new Error(`[modern-admin/feature-upload] refusing to upload with unsafe key "${key}"`);
74
+ }
68
75
  const c = await this.client();
69
76
  const ext = extname(file.originalName);
70
77
  const prefix = this.options.prefix ? `${this.options.prefix}/` : '';
@@ -109,12 +116,19 @@ export class S3UploadProvider {
109
116
  return resolvedKey;
110
117
  }
111
118
  async getUrl(key) {
119
+ if (isUnsafeKey(key)) {
120
+ throw new Error(`[modern-admin/feature-upload] refusing to build a URL for unsafe key "${key}"`);
121
+ }
112
122
  if (this.options.signed) {
113
123
  return this.signedUrl(key);
114
124
  }
115
125
  return `${this.publicBaseUrl()}/${key}`;
116
126
  }
117
127
  async delete(key) {
128
+ // A `type: 'file'` value is an arbitrary DB string — never issue a
129
+ // DeleteObject for a traversal/absolute key.
130
+ if (isUnsafeKey(key))
131
+ return;
118
132
  try {
119
133
  const sdk = await import('@aws-sdk/client-s3');
120
134
  const c = await this.client();
@@ -1 +1 @@
1
- {"version":3,"file":"s3.js","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"s3.js","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"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"}
@@ -17,6 +17,14 @@ export interface RegisteredUploadConfig {
17
17
  uploadPath?: (filename: string) => string;
18
18
  /** True for multi-file properties — controller will accept N files per request. */
19
19
  isArray?: boolean;
20
+ /**
21
+ * Allowed MIME patterns (HTML `accept` syntax). Enforced server-side by the
22
+ * upload controller / GraphQL resolver — a request whose file declares a
23
+ * non-matching type is rejected. `undefined`/empty means no restriction.
24
+ */
25
+ mimeTypes?: string[];
26
+ /** Maximum accepted file size in bytes. Enforced server-side (per file). */
27
+ maxSize?: number;
20
28
  }
21
29
  export declare const UploadProviderRegistry: {
22
30
  register(id: string, config: RegisteredUploadConfig): void;
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,0DAA0D;AAC1D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,eAAe,CAAA;IACzB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAA;IACzC,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAID,eAAO,MAAM,sBAAsB;iBACpB,MAAM,UAAU,sBAAsB,GAAG,IAAI;YAIlD,MAAM,GAAG,sBAAsB,GAAG,SAAS;IAInD,wBAAwB;aACf,IAAI;CAGd,CAAA"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,0DAA0D;AAC1D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,eAAe,CAAA;IACzB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAA;IACzC,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAID,eAAO,MAAM,sBAAsB;iBACpB,MAAM,UAAU,sBAAsB,GAAG,IAAI;YAIlD,MAAM,GAAG,sBAAsB,GAAG,SAAS;IAInD,wBAAwB;aACf,IAAI;CAGd,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAaH,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkC,CAAA;AAE3D,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,QAAQ,CAAC,EAAU,EAAE,MAA8B;QACjD,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;IAC3B,CAAC;IAED,GAAG,CAAC,EAAU;QACZ,OAAO,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAC1B,CAAC;IAED,wBAAwB;IACxB,KAAK;QACH,SAAS,CAAC,KAAK,EAAE,CAAA;IACnB,CAAC;CACF,CAAA"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAqBH,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkC,CAAA;AAE3D,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,QAAQ,CAAC,EAAU,EAAE,MAA8B;QACjD,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;IAC3B,CAAC;IAED,GAAG,CAAC,EAAU;QACZ,OAAO,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAC1B,CAAC;IAED,wBAAwB;IACxB,KAAK;QACH,SAAS,CAAC,KAAK,EAAE,CAAA;IACnB,CAAC;CACF,CAAA"}
package/dist/types.d.ts CHANGED
@@ -76,10 +76,18 @@ export interface UploadPropertyConfig {
76
76
  /**
77
77
  * Allowed MIME type patterns (same syntax as the HTML `accept` attribute).
78
78
  * Examples: `['image/*']`, `['image/jpeg', 'application/pdf']`.
79
- * Enforcement is advisory on the frontend; the controller does not re-validate.
79
+ * Enforced both on the frontend (UX) and server-side the upload controller
80
+ * / GraphQL resolver rejects a file whose declared type does not match. The
81
+ * server match is on the *declared* Content-Type (defense-in-depth, not
82
+ * content sniffing).
80
83
  */
81
84
  mimeTypes?: string[];
82
- /** Maximum upload size in bytes. Advisory (frontend warning only). */
85
+ /**
86
+ * Maximum upload size in bytes. Enforced server-side per file (the multipart
87
+ * parser aborts an oversized stream) in addition to the frontend warning.
88
+ * A module-wide hard cap (`ModernAdminUploadModule.forRoot({ maxFileSize })`,
89
+ * default 25 MiB) always applies on top, even when this is unset.
90
+ */
83
91
  maxSize?: number;
84
92
  /**
85
93
  * Treat the property as an array of file keys (multi-file upload).
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,0DAA0D;AAC1D,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAA;IACpB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAA;IAChB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAA;IACX,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAA;IACX,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,iBAAiB;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAEzD;;;;OAIG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAE7C;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAElC;;;;;;OAMG;IACH,WAAW,CAAC,IAAI,MAAM,GAAG,SAAS,CAAA;CACnC;AAED,qEAAqE;AACrE,MAAM,WAAW,oBAAoB;IACnC,0CAA0C;IAC1C,QAAQ,EAAE,eAAe,CAAA;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C;AAED,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;;;;OAYG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;CACjD"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,0DAA0D;AAC1D,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAA;IACpB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAA;IAChB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAA;IACX,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAA;IACX,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,iBAAiB;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAEzD;;;;OAIG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAE7C;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAElC;;;;;;OAMG;IACH,WAAW,CAAC,IAAI,MAAM,GAAG,SAAS,CAAA;CACnC;AAED,qEAAqE;AACrE,MAAM,WAAW,oBAAoB;IACnC,0CAA0C;IAC1C,QAAQ,EAAE,eAAe,CAAA;IACzB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C;AAED,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;;;;OAYG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;CACjD"}
@@ -1 +1 @@
1
- {"version":3,"file":"upload-feature.d.ts","sourceRoot":"","sources":["../src/upload-feature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAmD,KAAK,SAAS,EAAwB,MAAM,oBAAoB,CAAA;AAC1H,OAAO,KAAK,EAAE,oBAAoB,EAAwB,MAAM,YAAY,CAAA;AAiD5E,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAiItE"}
1
+ {"version":3,"file":"upload-feature.d.ts","sourceRoot":"","sources":["../src/upload-feature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAoE,KAAK,SAAS,EAAwB,MAAM,oBAAoB,CAAA;AAC3I,OAAO,KAAK,EAAE,oBAAoB,EAAwB,MAAM,YAAY,CAAA;AA8B5E,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CA+ItE"}
@@ -25,24 +25,9 @@
25
25
  * },
26
26
  * })
27
27
  */
28
- import { uuidv7 } from '@modern-admin/core';
28
+ import { appendAfterHook, uuidv7 } from '@modern-admin/core';
29
29
  import { UploadProviderRegistry } from './registry.js';
30
30
  import { PendingUploadsRegistry } from './pending-registry.js';
31
- /** Normalise a hook value (fn | fn[] | undefined) into an array. */
32
- function toArray(hook) {
33
- if (!hook)
34
- return [];
35
- return Array.isArray(hook) ? hook : [hook];
36
- }
37
- /**
38
- * Merge `after` hooks from an existing action override with new hooks,
39
- * producing an array that runs all of them in order.
40
- * If `existing` already defines `after`, the new hooks are appended.
41
- */
42
- function mergeAfterHook(existing, newHook) {
43
- const existing_after = toArray(existing?.after);
44
- return [...existing_after, newHook];
45
- }
46
31
  // ─── Value helpers ────────────────────────────────────────────────────────────
47
32
  /** Return non-empty file keys for the given value (handles single + array). */
48
33
  function toKeys(value) {
@@ -55,18 +40,32 @@ function toKeys(value) {
55
40
  }
56
41
  // ─── Feature function ─────────────────────────────────────────────────────────
57
42
  export function uploadFeature(options) {
58
- // Register configs immediately before the FeatureFn is called.
59
- const registered = new Map();
60
- for (const [propPath, config] of Object.entries(options.properties)) {
61
- const providerId = `up_${uuidv7().replace(/-/g, '')}`;
62
- UploadProviderRegistry.register(providerId, {
63
- provider: config.provider,
64
- uploadPath: config.uploadPath,
65
- isArray: config.isArray ?? false,
66
- });
67
- registered.set(propPath, { providerId, config });
68
- }
69
- return (resourceOptions) => {
43
+ // Precompute per-property config plus a process-local fallback id, used only
44
+ // when the FeatureFn is invoked without a resource (e.g. unit tests calling
45
+ // it directly). In production the id is derived from `resource.id()` below.
46
+ const props = Object.entries(options.properties).map(([propPath, config]) => ({
47
+ propPath,
48
+ config,
49
+ fallbackId: `up_${uuidv7().replace(/-/g, '')}`,
50
+ }));
51
+ return (resourceOptions, resource) => {
52
+ // Register providers with a DETERMINISTIC id derived from the resource id
53
+ // + property path. Every replica behind a load balancer computes the same
54
+ // id, so a property served by replica A resolves on replica B's registry
55
+ // (a per-process UUID would 500 on the "wrong" replica). Registration
56
+ // happens here (at bootstrap decorate time), before any request is served.
57
+ const registered = new Map();
58
+ for (const { propPath, config, fallbackId } of props) {
59
+ const providerId = resource ? `up_${resource.id()}_${propPath}` : fallbackId;
60
+ UploadProviderRegistry.register(providerId, {
61
+ provider: config.provider,
62
+ uploadPath: config.uploadPath,
63
+ isArray: config.isArray ?? false,
64
+ ...(config.mimeTypes ? { mimeTypes: config.mimeTypes } : {}),
65
+ ...(config.maxSize != null ? { maxSize: config.maxSize } : {}),
66
+ });
67
+ registered.set(propPath, { providerId, config });
68
+ }
70
69
  // --- Property overrides ---
71
70
  const propOverrides = {};
72
71
  for (const [propPath, { providerId, config }] of registered) {
@@ -146,15 +145,15 @@ export function uploadFeature(options) {
146
145
  const actionOverrides = {
147
146
  new: {
148
147
  ...existingNew,
149
- after: mergeAfterHook(existingNew, newAfterHook),
148
+ after: appendAfterHook(existingNew, newAfterHook),
150
149
  },
151
150
  edit: {
152
151
  ...existingEdit,
153
- after: mergeAfterHook(existingEdit, editAfterHook),
152
+ after: appendAfterHook(existingEdit, editAfterHook),
154
153
  },
155
154
  delete: {
156
155
  ...existingDelete,
157
- after: mergeAfterHook(existingDelete, deleteAfterHook),
156
+ after: appendAfterHook(existingDelete, deleteAfterHook),
158
157
  },
159
158
  };
160
159
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"upload-feature.js","sourceRoot":"","sources":["../src/upload-feature.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"upload-feature.js","sourceRoot":"","sources":["../src/upload-feature.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modern-admin/feature-upload",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "File upload feature plugin for Modern Admin (local + S3 providers).",
5
5
  "homepage": "https://github.com/modern-admin/modern-admin/tree/main/packages/feature-upload#readme",
6
6
  "keywords": [
@@ -14,6 +14,7 @@
14
14
  "feature"
15
15
  ],
16
16
  "type": "module",
17
+ "sideEffects": false,
17
18
  "license": "MIT",
18
19
  "repository": {
19
20
  "type": "git",
@@ -61,7 +62,7 @@
61
62
  }
62
63
  },
63
64
  "dependencies": {
64
- "@modern-admin/core": "^0.2.1",
65
+ "@modern-admin/core": "^0.3.0",
65
66
  "@nestjs/common": "^11.1.19",
66
67
  "@nestjs/core": "^11.1.19",
67
68
  "busboy": "^1.6.0",
@@ -69,19 +70,19 @@
69
70
  "rxjs": "^7.8.2"
70
71
  },
71
72
  "devDependencies": {
72
- "@modern-admin/graphql": "0.2.1",
73
- "@modern-admin/nest": "0.2.1",
74
- "@modern-admin/tsconfig": "0.2.1",
73
+ "@modern-admin/graphql": "0.3.0",
74
+ "@modern-admin/nest": "0.3.0",
75
+ "@modern-admin/tsconfig": "0.3.0",
75
76
  "@nestjs/swagger": "^11.4.2",
76
77
  "@types/bun": "^1.3.13",
77
78
  "@types/busboy": "^1.5.4",
78
- "graphql": "^16.11.0",
79
+ "graphql": "^17.0.2",
79
80
  "typescript": "^6.0.3"
80
81
  },
81
82
  "peerDependencies": {
82
83
  "@aws-sdk/client-s3": "^3.0.0",
83
84
  "@nestjs/swagger": "^11.4.2",
84
- "graphql": "^16.0.0"
85
+ "graphql": "^17.0.0"
85
86
  },
86
87
  "peerDependenciesMeta": {
87
88
  "@aws-sdk/client-s3": {
package/src/graphql.ts CHANGED
@@ -39,6 +39,8 @@ import {
39
39
  import { ForbiddenError, ResourceNotFoundError } from '@modern-admin/core'
40
40
  import { UploadProviderRegistry } from './registry.js'
41
41
  import { PendingUploadsRegistry } from './pending-registry.js'
42
+ import { isUnsafeKey, sanitizeFilename } from './path-safety.js'
43
+ import { mimeMatches } from './mime.js'
42
44
  import type { UploadedFileInfo } from './types.js'
43
45
 
44
46
  /** Default TTL for pending upload entries, mirrored from the controller. */
@@ -93,12 +95,26 @@ export function uploadGraphqlExtension(
93
95
  size: number
94
96
  buffer: Buffer
95
97
  }
98
+ // Server-side MIME / size enforcement (the multipart parser has
99
+ // already applied the global hard size cap; this adds the tighter
100
+ // per-property limits).
101
+ if (!mimeMatches(upload.mimeType, registered.mimeTypes)) {
102
+ throw new Error(`File type "${upload.mimeType}" is not allowed`)
103
+ }
104
+ if (registered.maxSize != null && upload.size > registered.maxSize) {
105
+ throw new Error(`File exceeds the maximum size of ${registered.maxSize} bytes`)
106
+ }
107
+ // Sanitise the client filename before it can steer key generation.
108
+ const originalName = sanitizeFilename(upload.filename)
96
109
  const computedKey = registered.uploadPath
97
- ? registered.uploadPath(upload.filename)
110
+ ? registered.uploadPath(originalName)
98
111
  : undefined
112
+ if (computedKey !== undefined && isUnsafeKey(computedKey)) {
113
+ throw new Error('Computed upload key is not allowed')
114
+ }
99
115
  const key = await registered.provider.upload(
100
116
  {
101
- originalName: upload.filename,
117
+ originalName,
102
118
  mimeType: upload.mimeType,
103
119
  size: upload.size,
104
120
  buffer: upload.buffer,
@@ -110,7 +126,7 @@ export function uploadGraphqlExtension(
110
126
  const info: UploadedFileInfo = {
111
127
  key,
112
128
  url,
113
- name: upload.filename,
129
+ name: originalName,
114
130
  size: upload.size,
115
131
  mimeType: upload.mimeType,
116
132
  }
@@ -137,6 +153,7 @@ export function uploadGraphqlExtension(
137
153
  // the REST controller's check — prevents leaking arbitrary
138
154
  // resource ids to authenticated clients.
139
155
  resolveProperty(ctx, resourceId, field)
156
+ if (isUnsafeKey(key)) return false
140
157
  return PendingUploadsRegistry.cancel(key)
141
158
  },
142
159
  },
@@ -160,8 +177,8 @@ function resolveProperty(
160
177
  try {
161
178
  resource = ctx.admin.findResource(resourceId)
162
179
  } catch (err) {
163
- if (err instanceof ResourceNotFoundError) throw new Error(err.message)
164
- if (err instanceof ForbiddenError) throw new Error(err.message)
180
+ if (err instanceof ResourceNotFoundError) throw new Error(err.message, { cause: err })
181
+ if (err instanceof ForbiddenError) throw new Error(err.message, { cause: err })
165
182
  throw err
166
183
  }
167
184
  const decorator = resource.decorate()
package/src/index.ts CHANGED
@@ -8,6 +8,11 @@ export { uploadFeature } from './upload-feature.js'
8
8
  export { UploadProviderRegistry } from './registry.js'
9
9
  export { PendingUploadsRegistry } from './pending-registry.js'
10
10
 
11
+ // Path-safety helpers (shared by providers + custom upload backends).
12
+ export { isUnsafeKey, sanitizeFilename, resolveWithinDir } from './path-safety.js'
13
+ // Server-side MIME allow-list matcher (HTML `accept` syntax).
14
+ export { mimeMatches } from './mime.js'
15
+
11
16
  // Built-in providers
12
17
  export { LocalUploadProvider, type LocalUploadOptions } from './providers/local.js'
13
18
  export { S3UploadProvider, type S3UploadOptions } from './providers/s3.js'