v0.0.2-alpha.13
Pre-releaseReleased 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
f943cb3Thanks @aqib-rx! - Unified upload validation across both upload paths./api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith 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 fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose 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: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.Breaking:
UploadService.upload()now throwsNextlyError.validationon validation failures instead of returning{ success: false, errors, … }— storage-layer 5xx failures still return the result-shape./api/mediarejects files outside the default MIME allowlist (override viasecurity.uploads.allowedMimeTypesoradditionalMimeTypes). SVG uploads with<foreignObject>, externalhref, animations,<style>blocks, ordata:URIs will have those elements stripped — sanitized output may differ from input.@nextlyhq/storage-vercel-blobnow supports SVG uploads (previously refused). The adapter returns Vercel Blob'sdownloadUrl(the file URL with?download=1appended) when the upload requestscontentDisposition: "attachment", so direct top-level navigation forces an attachment download while<img src>rendering remains unaffected. HTML uploads continue to be rejected withNextlyError.validation(codeUNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition.storage-localcannot 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.rejectedis emitted on every validation failure with{ code, route, mimeType, filename, size }so operators can alert on attack-pattern spikes (sudden bursts ofMAGIC_BYTE_MISMATCHorEXTENSION_BLOCKEDindicate polyglot probing).Build/dependency: the
pnpm.overridesblock now bumpsundicito^7to fix a pre-existing latent runtime bug —jsdom@28(a transitive dep ofisomorphic-dompurify) requiresundici@7+'slib/handler/wrap-handler.js, but the workspace was resolvingundici@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
f943cb3Thanks @aqib-rx! - Unified upload validation across both upload paths./api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith 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 fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose 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: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.Breaking:
UploadService.upload()now throwsNextlyError.validationon validation failures instead of returning{ success: false, errors, … }— storage-layer 5xx failures still return the result-shape./api/mediarejects files outside the default MIME allowlist (override viasecurity.uploads.allowedMimeTypesoradditionalMimeTypes). SVG uploads with<foreignObject>, externalhref, animations,<style>blocks, ordata:URIs will have those elements stripped — sanitized output may differ from input.@nextlyhq/storage-vercel-blobnow supports SVG uploads (previously refused). The adapter returns Vercel Blob'sdownloadUrl(the file URL with?download=1appended) when the upload requestscontentDisposition: "attachment", so direct top-level navigation forces an attachment download while<img src>rendering remains unaffected. HTML uploads continue to be rejected withNextlyError.validation(codeUNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition.storage-localcannot 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.rejectedis emitted on every validation failure with{ code, route, mimeType, filename, size }so operators can alert on attack-pattern spikes (sudden bursts ofMAGIC_BYTE_MISMATCHorEXTENSION_BLOCKEDindicate polyglot probing).Build/dependency: the
pnpm.overridesblock now bumpsundicito^7to fix a pre-existing latent runtime bug —jsdom@28(a transitive dep ofisomorphic-dompurify) requiresundici@7+'slib/handler/wrap-handler.js, but the workspace was resolvingundici@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
f943cb3Thanks @aqib-rx! - Unified upload validation across both upload paths./api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith 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 fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose 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: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.Breaking:
UploadService.upload()now throwsNextlyError.validationon validation failures instead of returning{ success: false, errors, … }— storage-layer 5xx failures still return the result-shape./api/mediarejects files outside the default MIME allowlist (override viasecurity.uploads.allowedMimeTypesoradditionalMimeTypes). SVG uploads with<foreignObject>, externalhref, animations,<style>blocks, ordata:URIs will have those elements stripped — sanitized output may differ from input.@nextlyhq/storage-vercel-blobnow supports SVG uploads (previously refused). The adapter returns Vercel Blob'sdownloadUrl(the file URL with?download=1appended) when the upload requestscontentDisposition: "attachment", so direct top-level navigation forces an attachment download while<img src>rendering remains unaffected. HTML uploads continue to be rejected withNextlyError.validation(codeUNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition.storage-localcannot 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.rejectedis emitted on every validation failure with{ code, route, mimeType, filename, size }so operators can alert on attack-pattern spikes (sudden bursts ofMAGIC_BYTE_MISMATCHorEXTENSION_BLOCKEDindicate polyglot probing).Build/dependency: the
pnpm.overridesblock now bumpsundicito^7to fix a pre-existing latent runtime bug —jsdom@28(a transitive dep ofisomorphic-dompurify) requiresundici@7+'slib/handler/wrap-handler.js, but the workspace was resolvingundici@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
f943cb3Thanks @aqib-rx! - Unified upload validation across both upload paths./api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith 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 fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose 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: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.Breaking:
UploadService.upload()now throwsNextlyError.validationon validation failures instead of returning{ success: false, errors, … }— storage-layer 5xx failures still return the result-shape./api/mediarejects files outside the default MIME allowlist (override viasecurity.uploads.allowedMimeTypesoradditionalMimeTypes). SVG uploads with<foreignObject>, externalhref, animations,<style>blocks, ordata:URIs will have those elements stripped — sanitized output may differ from input.@nextlyhq/storage-vercel-blobnow supports SVG uploads (previously refused). The adapter returns Vercel Blob'sdownloadUrl(the file URL with?download=1appended) when the upload requestscontentDisposition: "attachment", so direct top-level navigation forces an attachment download while<img src>rendering remains unaffected. HTML uploads continue to be rejected withNextlyError.validation(codeUNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition.storage-localcannot 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.rejectedis emitted on every validation failure with{ code, route, mimeType, filename, size }so operators can alert on attack-pattern spikes (sudden bursts ofMAGIC_BYTE_MISMATCHorEXTENSION_BLOCKEDindicate polyglot probing).Build/dependency: the
pnpm.overridesblock now bumpsundicito^7to fix a pre-existing latent runtime bug —jsdom@28(a transitive dep ofisomorphic-dompurify) requiresundici@7+'slib/handler/wrap-handler.js, but the workspace was resolvingundici@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
f943cb3Thanks @aqib-rx! - Unified upload validation across both upload paths./api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith 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 fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose 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: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.Breaking:
UploadService.upload()now throwsNextlyError.validationon validation failures instead of returning{ success: false, errors, … }— storage-layer 5xx failures still return the result-shape./api/mediarejects files outside the default MIME allowlist (override viasecurity.uploads.allowedMimeTypesoradditionalMimeTypes). SVG uploads with<foreignObject>, externalhref, animations,<style>blocks, ordata:URIs will have those elements stripped — sanitized output may differ from input.@nextlyhq/storage-vercel-blobnow supports SVG uploads (previously refused). The adapter returns Vercel Blob'sdownloadUrl(the file URL with?download=1appended) when the upload requestscontentDisposition: "attachment", so direct top-level navigation forces an attachment download while<img src>rendering remains unaffected. HTML uploads continue to be rejected withNextlyError.validation(codeUNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition.storage-localcannot 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.rejectedis emitted on every validation failure with{ code, route, mimeType, filename, size }so operators can alert on attack-pattern spikes (sudden bursts ofMAGIC_BYTE_MISMATCHorEXTENSION_BLOCKEDindicate polyglot probing).Build/dependency: the
pnpm.overridesblock now bumpsundicito^7to fix a pre-existing latent runtime bug —jsdom@28(a transitive dep ofisomorphic-dompurify) requiresundici@7+'slib/handler/wrap-handler.js, but the workspace was resolvingundici@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
f943cb3Thanks @aqib-rx! - Unified upload validation across both upload paths./api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith 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 fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose 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: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.Breaking:
UploadService.upload()now throwsNextlyError.validationon validation failures instead of returning{ success: false, errors, … }— storage-layer 5xx failures still return the result-shape./api/mediarejects files outside the default MIME allowlist (override viasecurity.uploads.allowedMimeTypesoradditionalMimeTypes). SVG uploads with<foreignObject>, externalhref, animations,<style>blocks, ordata:URIs will have those elements stripped — sanitized output may differ from input.@nextlyhq/storage-vercel-blobnow supports SVG uploads (previously refused). The adapter returns Vercel Blob'sdownloadUrl(the file URL with?download=1appended) when the upload requestscontentDisposition: "attachment", so direct top-level navigation forces an attachment download while<img src>rendering remains unaffected. HTML uploads continue to be rejected withNextlyError.validation(codeUNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition.storage-localcannot 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.rejectedis emitted on every validation failure with{ code, route, mimeType, filename, size }so operators can alert on attack-pattern spikes (sudden bursts ofMAGIC_BYTE_MISMATCHorEXTENSION_BLOCKEDindicate polyglot probing).Build/dependency: the
pnpm.overridesblock now bumpsundicito^7to fix a pre-existing latent runtime bug —jsdom@28(a transitive dep ofisomorphic-dompurify) requiresundici@7+'slib/handler/wrap-handler.js, but the workspace was resolvingundici@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
f943cb3Thanks @aqib-rx! - Unified upload validation across both upload paths./api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith 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 fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose 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: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.Breaking:
UploadService.upload()now throwsNextlyError.validationon validation failures instead of returning{ success: false, errors, … }— storage-layer 5xx failures still return the result-shape./api/mediarejects files outside the default MIME allowlist (override viasecurity.uploads.allowedMimeTypesoradditionalMimeTypes). SVG uploads with<foreignObject>, externalhref, animations,<style>blocks, ordata:URIs will have those elements stripped — sanitized output may differ from input.@nextlyhq/storage-vercel-blobnow supports SVG uploads (previously refused). The adapter returns Vercel Blob'sdownloadUrl(the file URL with?download=1appended) when the upload requestscontentDisposition: "attachment", so direct top-level navigation forces an attachment download while<img src>rendering remains unaffected. HTML uploads continue to be rejected withNextlyError.validation(codeUNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition.storage-localcannot 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.rejectedis emitted on every validation failure with{ code, route, mimeType, filename, size }so operators can alert on attack-pattern spikes (sudden bursts ofMAGIC_BYTE_MISMATCHorEXTENSION_BLOCKEDindicate polyglot probing).Build/dependency: the
pnpm.overridesblock now bumpsundicito^7to fix a pre-existing latent runtime bug —jsdom@28(a transitive dep ofisomorphic-dompurify) requiresundici@7+'slib/handler/wrap-handler.js, but the workspace was resolvingundici@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
f943cb3Thanks @aqib-rx! - Unified upload validation across both upload paths./api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith 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 fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose 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: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.Breaking:
UploadService.upload()now throwsNextlyError.validationon validation failures instead of returning{ success: false, errors, … }— storage-layer 5xx failures still return the result-shape./api/mediarejects files outside the default MIME allowlist (override viasecurity.uploads.allowedMimeTypesoradditionalMimeTypes). SVG uploads with<foreignObject>, externalhref, animations,<style>blocks, ordata:URIs will have those elements stripped — sanitized output may differ from input.@nextlyhq/storage-vercel-blobnow supports SVG uploads (previously refused). The adapter returns Vercel Blob'sdownloadUrl(the file URL with?download=1appended) when the upload requestscontentDisposition: "attachment", so direct top-level navigation forces an attachment download while<img src>rendering remains unaffected. HTML uploads continue to be rejected withNextlyError.validation(codeUNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition.storage-localcannot 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.rejectedis emitted on every validation failure with{ code, route, mimeType, filename, size }so operators can alert on attack-pattern spikes (sudden bursts ofMAGIC_BYTE_MISMATCHorEXTENSION_BLOCKEDindicate polyglot probing).Build/dependency: the
pnpm.overridesblock now bumpsundicito^7to fix a pre-existing latent runtime bug —jsdom@28(a transitive dep ofisomorphic-dompurify) requiresundici@7+'slib/handler/wrap-handler.js, but the workspace was resolvingundici@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
f943cb3Thanks @aqib-rx! - Unified upload validation across both upload paths./api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith 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 fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose 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: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.Breaking:
UploadService.upload()now throwsNextlyError.validationon validation failures instead of returning{ success: false, errors, … }— storage-layer 5xx failures still return the result-shape./api/mediarejects files outside the default MIME allowlist (override viasecurity.uploads.allowedMimeTypesoradditionalMimeTypes). SVG uploads with<foreignObject>, externalhref, animations,<style>blocks, ordata:URIs will have those elements stripped — sanitized output may differ from input.@nextlyhq/storage-vercel-blobnow supports SVG uploads (previously refused). The adapter returns Vercel Blob'sdownloadUrl(the file URL with?download=1appended) when the upload requestscontentDisposition: "attachment", so direct top-level navigation forces an attachment download while<img src>rendering remains unaffected. HTML uploads continue to be rejected withNextlyError.validation(codeUNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition.storage-localcannot 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.rejectedis emitted on every validation failure with{ code, route, mimeType, filename, size }so operators can alert on attack-pattern spikes (sudden bursts ofMAGIC_BYTE_MISMATCHorEXTENSION_BLOCKEDindicate polyglot probing).Build/dependency: the
pnpm.overridesblock now bumpsundicito^7to fix a pre-existing latent runtime bug —jsdom@28(a transitive dep ofisomorphic-dompurify) requiresundici@7+'slib/handler/wrap-handler.js, but the workspace was resolvingundici@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
f943cb3Thanks @aqib-rx! - Unified upload validation across both upload paths./api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith 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 fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose 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: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.Breaking:
UploadService.upload()now throwsNextlyError.validationon validation failures instead of returning{ success: false, errors, … }— storage-layer 5xx failures still return the result-shape./api/mediarejects files outside the default MIME allowlist (override viasecurity.uploads.allowedMimeTypesoradditionalMimeTypes). SVG uploads with<foreignObject>, externalhref, animations,<style>blocks, ordata:URIs will have those elements stripped — sanitized output may differ from input.@nextlyhq/storage-vercel-blobnow supports SVG uploads (previously refused). The adapter returns Vercel Blob'sdownloadUrl(the file URL with?download=1appended) when the upload requestscontentDisposition: "attachment", so direct top-level navigation forces an attachment download while<img src>rendering remains unaffected. HTML uploads continue to be rejected withNextlyError.validation(codeUNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition.storage-localcannot 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.rejectedis emitted on every validation failure with{ code, route, mimeType, filename, size }so operators can alert on attack-pattern spikes (sudden bursts ofMAGIC_BYTE_MISMATCHorEXTENSION_BLOCKEDindicate polyglot probing).Build/dependency: the
pnpm.overridesblock now bumpsundicito^7to fix a pre-existing latent runtime bug —jsdom@28(a transitive dep ofisomorphic-dompurify) requiresundici@7+'slib/handler/wrap-handler.js, but the workspace was resolvingundici@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
f943cb3Thanks @aqib-rx! - Unified upload validation across both upload paths./api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith 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 fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose 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: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.Breaking:
UploadService.upload()now throwsNextlyError.validationon validation failures instead of returning{ success: false, errors, … }— storage-layer 5xx failures still return the result-shape./api/mediarejects files outside the default MIME allowlist (override viasecurity.uploads.allowedMimeTypesoradditionalMimeTypes). SVG uploads with<foreignObject>, externalhref, animations,<style>blocks, ordata:URIs will have those elements stripped — sanitized output may differ from input.@nextlyhq/storage-vercel-blobnow supports SVG uploads (previously refused). The adapter returns Vercel Blob'sdownloadUrl(the file URL with?download=1appended) when the upload requestscontentDisposition: "attachment", so direct top-level navigation forces an attachment download while<img src>rendering remains unaffected. HTML uploads continue to be rejected withNextlyError.validation(codeUNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition.storage-localcannot 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.rejectedis emitted on every validation failure with{ code, route, mimeType, filename, size }so operators can alert on attack-pattern spikes (sudden bursts ofMAGIC_BYTE_MISMATCHorEXTENSION_BLOCKEDindicate polyglot probing).Build/dependency: the
pnpm.overridesblock now bumpsundicito^7to fix a pre-existing latent runtime bug —jsdom@28(a transitive dep ofisomorphic-dompurify) requiresundici@7+'slib/handler/wrap-handler.js, but the workspace was resolvingundici@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
f943cb3Thanks @aqib-rx! - Unified upload validation across both upload paths./api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith 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 fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose 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: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.Breaking:
UploadService.upload()now throwsNextlyError.validationon validation failures instead of returning{ success: false, errors, … }— storage-layer 5xx failures still return the result-shape./api/mediarejects files outside the default MIME allowlist (override viasecurity.uploads.allowedMimeTypesoradditionalMimeTypes). SVG uploads with<foreignObject>, externalhref, animations,<style>blocks, ordata:URIs will have those elements stripped — sanitized output may differ from input.@nextlyhq/storage-vercel-blobnow supports SVG uploads (previously refused). The adapter returns Vercel Blob'sdownloadUrl(the file URL with?download=1appended) when the upload requestscontentDisposition: "attachment", so direct top-level navigation forces an attachment download while<img src>rendering remains unaffected. HTML uploads continue to be rejected withNextlyError.validation(codeUNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition.storage-localcannot 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.rejectedis emitted on every validation failure with{ code, route, mimeType, filename, size }so operators can alert on attack-pattern spikes (sudden bursts ofMAGIC_BYTE_MISMATCHorEXTENSION_BLOCKEDindicate polyglot probing).Build/dependency: the
pnpm.overridesblock now bumpsundicito^7to fix a pre-existing latent runtime bug —jsdom@28(a transitive dep ofisomorphic-dompurify) requiresundici@7+'slib/handler/wrap-handler.js, but the workspace was resolvingundici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.