Skip to content

v0.0.2-alpha.13

Pre-release
Pre-release

Choose a tag to compare

@mobeenabdullah mobeenabdullah released this 20 May 08:23
· 220 commits to main since this release
4c77516

Released all 12 packages at 0.0.2-alpha.13 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).

What's changed

@nextlyhq/adapter-drizzle

Patch Changes

  • #46 f943cb3 Thanks @aqib-rx! - Unified upload validation across both upload paths. /api/media now applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that /admin/api/collections/[slug]/uploads already had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted into services/upload-validation/, both UploadService and MediaService call its validateAndSanitizeUpload entrypoint, and every validation failure now throws NextlyError.validation with a stable machine code (FILENAME_INVALID, EXTENSION_BLOCKED, MIME_BLOCKED, MIME_NOT_ALLOWED, SIZE_EXCEEDED, MAGIC_BYTE_MISMATCH, SVG_SANITIZATION_FAILED, UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened from USE_PROFILES: { svg, svgFilters } alone to explicit FORBID_TAGS (foreignObject, animate*, image, iframe, object, embed, audio, video, source, track, style) plus FORBID_ATTR (event handlers, formaction, xlink:show/actuate) and an uponSanitizeAttribute hook that strips any href/xlink:href whose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claiming image/svg+xml with non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.

    Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.

    A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).

    Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.

@nextlyhq/adapter-mysql

Patch Changes

  • #46 f943cb3 Thanks @aqib-rx! - Unified upload validation across both upload paths. /api/media now applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that /admin/api/collections/[slug]/uploads already had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted into services/upload-validation/, both UploadService and MediaService call its validateAndSanitizeUpload entrypoint, and every validation failure now throws NextlyError.validation with a stable machine code (FILENAME_INVALID, EXTENSION_BLOCKED, MIME_BLOCKED, MIME_NOT_ALLOWED, SIZE_EXCEEDED, MAGIC_BYTE_MISMATCH, SVG_SANITIZATION_FAILED, UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened from USE_PROFILES: { svg, svgFilters } alone to explicit FORBID_TAGS (foreignObject, animate*, image, iframe, object, embed, audio, video, source, track, style) plus FORBID_ATTR (event handlers, formaction, xlink:show/actuate) and an uponSanitizeAttribute hook that strips any href/xlink:href whose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claiming image/svg+xml with non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.

    Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.

    A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).

    Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.

  • Updated dependencies [f943cb3]:

    • @nextlyhq/adapter-drizzle@0.0.2-alpha.13

@nextlyhq/adapter-postgres

Patch Changes

  • #46 f943cb3 Thanks @aqib-rx! - Unified upload validation across both upload paths. /api/media now applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that /admin/api/collections/[slug]/uploads already had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted into services/upload-validation/, both UploadService and MediaService call its validateAndSanitizeUpload entrypoint, and every validation failure now throws NextlyError.validation with a stable machine code (FILENAME_INVALID, EXTENSION_BLOCKED, MIME_BLOCKED, MIME_NOT_ALLOWED, SIZE_EXCEEDED, MAGIC_BYTE_MISMATCH, SVG_SANITIZATION_FAILED, UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened from USE_PROFILES: { svg, svgFilters } alone to explicit FORBID_TAGS (foreignObject, animate*, image, iframe, object, embed, audio, video, source, track, style) plus FORBID_ATTR (event handlers, formaction, xlink:show/actuate) and an uponSanitizeAttribute hook that strips any href/xlink:href whose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claiming image/svg+xml with non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.

    Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.

    A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).

    Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.

  • Updated dependencies [f943cb3]:

    • @nextlyhq/adapter-drizzle@0.0.2-alpha.13

@nextlyhq/adapter-sqlite

Patch Changes

  • #46 f943cb3 Thanks @aqib-rx! - Unified upload validation across both upload paths. /api/media now applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that /admin/api/collections/[slug]/uploads already had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted into services/upload-validation/, both UploadService and MediaService call its validateAndSanitizeUpload entrypoint, and every validation failure now throws NextlyError.validation with a stable machine code (FILENAME_INVALID, EXTENSION_BLOCKED, MIME_BLOCKED, MIME_NOT_ALLOWED, SIZE_EXCEEDED, MAGIC_BYTE_MISMATCH, SVG_SANITIZATION_FAILED, UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened from USE_PROFILES: { svg, svgFilters } alone to explicit FORBID_TAGS (foreignObject, animate*, image, iframe, object, embed, audio, video, source, track, style) plus FORBID_ATTR (event handlers, formaction, xlink:show/actuate) and an uponSanitizeAttribute hook that strips any href/xlink:href whose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claiming image/svg+xml with non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.

    Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.

    A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).

    Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.

  • Updated dependencies [f943cb3]:

    • @nextlyhq/adapter-drizzle@0.0.2-alpha.13

@nextlyhq/admin

Patch Changes

  • #46 f943cb3 Thanks @aqib-rx! - Unified upload validation across both upload paths. /api/media now applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that /admin/api/collections/[slug]/uploads already had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted into services/upload-validation/, both UploadService and MediaService call its validateAndSanitizeUpload entrypoint, and every validation failure now throws NextlyError.validation with a stable machine code (FILENAME_INVALID, EXTENSION_BLOCKED, MIME_BLOCKED, MIME_NOT_ALLOWED, SIZE_EXCEEDED, MAGIC_BYTE_MISMATCH, SVG_SANITIZATION_FAILED, UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened from USE_PROFILES: { svg, svgFilters } alone to explicit FORBID_TAGS (foreignObject, animate*, image, iframe, object, embed, audio, video, source, track, style) plus FORBID_ATTR (event handlers, formaction, xlink:show/actuate) and an uponSanitizeAttribute hook that strips any href/xlink:href whose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claiming image/svg+xml with non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.

    Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.

    A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).

    Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.

  • Updated dependencies [f943cb3]:

    • @nextlyhq/ui@0.0.2-alpha.13

create-nextly-app

Patch Changes

  • #46 f943cb3 Thanks @aqib-rx! - Unified upload validation across both upload paths. /api/media now applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that /admin/api/collections/[slug]/uploads already had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted into services/upload-validation/, both UploadService and MediaService call its validateAndSanitizeUpload entrypoint, and every validation failure now throws NextlyError.validation with a stable machine code (FILENAME_INVALID, EXTENSION_BLOCKED, MIME_BLOCKED, MIME_NOT_ALLOWED, SIZE_EXCEEDED, MAGIC_BYTE_MISMATCH, SVG_SANITIZATION_FAILED, UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened from USE_PROFILES: { svg, svgFilters } alone to explicit FORBID_TAGS (foreignObject, animate*, image, iframe, object, embed, audio, video, source, track, style) plus FORBID_ATTR (event handlers, formaction, xlink:show/actuate) and an uponSanitizeAttribute hook that strips any href/xlink:href whose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claiming image/svg+xml with non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.

    Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.

    A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).

    Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.

nextly

Patch Changes

  • #46 f943cb3 Thanks @aqib-rx! - Unified upload validation across both upload paths. /api/media now applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that /admin/api/collections/[slug]/uploads already had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted into services/upload-validation/, both UploadService and MediaService call its validateAndSanitizeUpload entrypoint, and every validation failure now throws NextlyError.validation with a stable machine code (FILENAME_INVALID, EXTENSION_BLOCKED, MIME_BLOCKED, MIME_NOT_ALLOWED, SIZE_EXCEEDED, MAGIC_BYTE_MISMATCH, SVG_SANITIZATION_FAILED, UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened from USE_PROFILES: { svg, svgFilters } alone to explicit FORBID_TAGS (foreignObject, animate*, image, iframe, object, embed, audio, video, source, track, style) plus FORBID_ATTR (event handlers, formaction, xlink:show/actuate) and an uponSanitizeAttribute hook that strips any href/xlink:href whose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claiming image/svg+xml with non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.

    Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.

    A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).

    Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.

  • Updated dependencies [f943cb3]:

    • @nextlyhq/adapter-drizzle@0.0.2-alpha.13
    • @nextlyhq/adapter-mysql@0.0.2-alpha.13
    • @nextlyhq/adapter-postgres@0.0.2-alpha.13
    • @nextlyhq/adapter-sqlite@0.0.2-alpha.13

@nextlyhq/plugin-form-builder

Patch Changes

  • #46 f943cb3 Thanks @aqib-rx! - Unified upload validation across both upload paths. /api/media now applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that /admin/api/collections/[slug]/uploads already had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted into services/upload-validation/, both UploadService and MediaService call its validateAndSanitizeUpload entrypoint, and every validation failure now throws NextlyError.validation with a stable machine code (FILENAME_INVALID, EXTENSION_BLOCKED, MIME_BLOCKED, MIME_NOT_ALLOWED, SIZE_EXCEEDED, MAGIC_BYTE_MISMATCH, SVG_SANITIZATION_FAILED, UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened from USE_PROFILES: { svg, svgFilters } alone to explicit FORBID_TAGS (foreignObject, animate*, image, iframe, object, embed, audio, video, source, track, style) plus FORBID_ATTR (event handlers, formaction, xlink:show/actuate) and an uponSanitizeAttribute hook that strips any href/xlink:href whose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claiming image/svg+xml with non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.

    Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.

    A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).

    Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.

  • Updated dependencies [f943cb3]:

    • @nextlyhq/admin@0.0.2-alpha.13
    • nextly@0.0.2-alpha.13
    • @nextlyhq/ui@0.0.2-alpha.13

@nextlyhq/storage-s3

Patch Changes

  • #46 f943cb3 Thanks @aqib-rx! - Unified upload validation across both upload paths. /api/media now applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that /admin/api/collections/[slug]/uploads already had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted into services/upload-validation/, both UploadService and MediaService call its validateAndSanitizeUpload entrypoint, and every validation failure now throws NextlyError.validation with a stable machine code (FILENAME_INVALID, EXTENSION_BLOCKED, MIME_BLOCKED, MIME_NOT_ALLOWED, SIZE_EXCEEDED, MAGIC_BYTE_MISMATCH, SVG_SANITIZATION_FAILED, UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened from USE_PROFILES: { svg, svgFilters } alone to explicit FORBID_TAGS (foreignObject, animate*, image, iframe, object, embed, audio, video, source, track, style) plus FORBID_ATTR (event handlers, formaction, xlink:show/actuate) and an uponSanitizeAttribute hook that strips any href/xlink:href whose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claiming image/svg+xml with non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.

    Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.

    A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).

    Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.

@nextlyhq/storage-uploadthing

Patch Changes

  • #46 f943cb3 Thanks @aqib-rx! - Unified upload validation across both upload paths. /api/media now applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that /admin/api/collections/[slug]/uploads already had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted into services/upload-validation/, both UploadService and MediaService call its validateAndSanitizeUpload entrypoint, and every validation failure now throws NextlyError.validation with a stable machine code (FILENAME_INVALID, EXTENSION_BLOCKED, MIME_BLOCKED, MIME_NOT_ALLOWED, SIZE_EXCEEDED, MAGIC_BYTE_MISMATCH, SVG_SANITIZATION_FAILED, UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened from USE_PROFILES: { svg, svgFilters } alone to explicit FORBID_TAGS (foreignObject, animate*, image, iframe, object, embed, audio, video, source, track, style) plus FORBID_ATTR (event handlers, formaction, xlink:show/actuate) and an uponSanitizeAttribute hook that strips any href/xlink:href whose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claiming image/svg+xml with non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.

    Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.

    A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).

    Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.

@nextlyhq/storage-vercel-blob

Patch Changes

  • #46 f943cb3 Thanks @aqib-rx! - Unified upload validation across both upload paths. /api/media now applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that /admin/api/collections/[slug]/uploads already had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted into services/upload-validation/, both UploadService and MediaService call its validateAndSanitizeUpload entrypoint, and every validation failure now throws NextlyError.validation with a stable machine code (FILENAME_INVALID, EXTENSION_BLOCKED, MIME_BLOCKED, MIME_NOT_ALLOWED, SIZE_EXCEEDED, MAGIC_BYTE_MISMATCH, SVG_SANITIZATION_FAILED, UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened from USE_PROFILES: { svg, svgFilters } alone to explicit FORBID_TAGS (foreignObject, animate*, image, iframe, object, embed, audio, video, source, track, style) plus FORBID_ATTR (event handlers, formaction, xlink:show/actuate) and an uponSanitizeAttribute hook that strips any href/xlink:href whose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claiming image/svg+xml with non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.

    Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.

    A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).

    Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.

@nextlyhq/ui

Patch Changes

  • #46 f943cb3 Thanks @aqib-rx! - Unified upload validation across both upload paths. /api/media now applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that /admin/api/collections/[slug]/uploads already had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted into services/upload-validation/, both UploadService and MediaService call its validateAndSanitizeUpload entrypoint, and every validation failure now throws NextlyError.validation with a stable machine code (FILENAME_INVALID, EXTENSION_BLOCKED, MIME_BLOCKED, MIME_NOT_ALLOWED, SIZE_EXCEEDED, MAGIC_BYTE_MISMATCH, SVG_SANITIZATION_FAILED, UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened from USE_PROFILES: { svg, svgFilters } alone to explicit FORBID_TAGS (foreignObject, animate*, image, iframe, object, embed, audio, video, source, track, style) plus FORBID_ATTR (event handlers, formaction, xlink:show/actuate) and an uponSanitizeAttribute hook that strips any href/xlink:href whose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claiming image/svg+xml with non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.

    Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.

    A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).

    Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.