Fix Decap image filename validation

This commit is contained in:
Kevin
2026-09-04 12:00:49 -04:00
parent 52c2568ab9
commit 2d5cdc8878
3 changed files with 16 additions and 8 deletions
+6
View File
@@ -156,6 +156,12 @@ not accept executables, archives, HTML, scripts, or SVG uploads. Asset rights,
alt text, crop, dimensions, and optimization remain operator review items even alt text, crop, dimensions, and optimization remain operator review items even
when the CMS accepts an upload. when the CMS accepts an upload.
Decap's image widget applies its field `pattern` to the selected file's base
name, not to the repository-relative media path. Keep that UI pattern limited
to the approved raster filename extensions; enforce the full
`public/images/uploads` path separately in the schema, verifier, and Gitea
branch rules below.
The media gallery lists only `public/images/uploads`, so the pilot seeds that The media gallery lists only `public/images/uploads`, so the pilot seeds that
directory with the images referenced by CMS-controlled fields. Removing an directory with the images referenced by CMS-controlled fields. Removing an
image from content clears the field and uses the page's text-only or initials image from content clears the field and uses the page's text-only or initials
+7 -7
View File
@@ -144,7 +144,7 @@ collections:
required: false required: false
choose_url: false choose_url: false
allow_multiple: false allow_multiple: false
pattern: ['^/images/uploads/[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.'] pattern: ['^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.']
media_library: media_library:
config: config:
max_file_size: 512000 max_file_size: 512000
@@ -181,7 +181,7 @@ collections:
required: false required: false
choose_url: false choose_url: false
allow_multiple: false allow_multiple: false
pattern: ['^/images/uploads/[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.'] pattern: ['^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.']
media_library: media_library:
config: { max_file_size: 512000, multiple: false } config: { max_file_size: 512000, multiple: false }
- label: Home hero image description - label: Home hero image description
@@ -194,7 +194,7 @@ collections:
required: false required: false
choose_url: false choose_url: false
allow_multiple: false allow_multiple: false
pattern: ['^/images/uploads/[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.'] pattern: ['^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.']
media_library: media_library:
config: { max_file_size: 512000, multiple: false } config: { max_file_size: 512000, multiple: false }
- label: Home story image description - label: Home story image description
@@ -207,7 +207,7 @@ collections:
required: false required: false
choose_url: false choose_url: false
allow_multiple: false allow_multiple: false
pattern: ['^/images/uploads/[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.'] pattern: ['^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.']
media_library: media_library:
config: { max_file_size: 512000, multiple: false } config: { max_file_size: 512000, multiple: false }
- label: About hero image description - label: About hero image description
@@ -220,7 +220,7 @@ collections:
required: false required: false
choose_url: false choose_url: false
allow_multiple: false allow_multiple: false
pattern: ['^/images/uploads/[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.'] pattern: ['^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.']
media_library: media_library:
config: { max_file_size: 512000, multiple: false } config: { max_file_size: 512000, multiple: false }
- label: About story image description - label: About story image description
@@ -233,7 +233,7 @@ collections:
required: false required: false
choose_url: false choose_url: false
allow_multiple: false allow_multiple: false
pattern: ['^/images/uploads/[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.'] pattern: ['^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.']
media_library: media_library:
config: { max_file_size: 512000, multiple: false } config: { max_file_size: 512000, multiple: false }
- label: Services hero image description - label: Services hero image description
@@ -246,7 +246,7 @@ collections:
required: false required: false
choose_url: false choose_url: false
allow_multiple: false allow_multiple: false
pattern: ['^/images/uploads/[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.'] pattern: ['^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp|avif)$', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.']
media_library: media_library:
config: { max_file_size: 512000, multiple: false } config: { max_file_size: 512000, multiple: false }
- label: Contact hero image description - label: Contact hero image description
+3 -1
View File
@@ -45,7 +45,7 @@ const markdownField = {
editor_components: [], editor_components: [],
}; };
const cmsImagePattern = [ const cmsImagePattern = [
'^/images/uploads/[A-Za-z0-9][A-Za-z0-9._-]*\\.(?:jpe?g|png|webp|avif)$', '^[A-Za-z0-9][A-Za-z0-9._-]*\\.(?:jpe?g|png|webp|avif)$',
'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.', 'Choose an approved JPG, PNG, WebP, or AVIF upload. SVG and general files are not allowed.',
]; ];
const optionalImageField = { const optionalImageField = {
@@ -160,6 +160,8 @@ allFields.forEach(visitField);
const imageFields = allFields.filter(({ widget }) => widget === 'image'); const imageFields = allFields.filter(({ widget }) => widget === 'image');
assert(imageFields.length === 7, 'Only the team image and six fixed page-image fields may expose media selection.'); assert(imageFields.length === 7, 'Only the team image and six fixed page-image fields may expose media selection.');
for (const field of imageFields) expectSubset(field, optionalImageField, `image field ${field.name}`); for (const field of imageFields) expectSubset(field, optionalImageField, `image field ${field.name}`);
assert(new RegExp(cmsImagePattern[0]).test('example-photo.jpg'), 'Image widget validation must accept the filename Decap passes to field patterns.');
assert(!new RegExp(cmsImagePattern[0]).test('example-photo.svg'), 'Image widget validation must reject SVG filenames.');
const schema = await readFile('src/content.config.ts', 'utf8'); const schema = await readFile('src/content.config.ts', 'utf8');
for (const fragment of [ for (const fragment of [