Stop Malicious Uploads at the Door — Upload Content Validation in the OXID Media Library Module

Upload content validation — files added to the media library are now checked for what they actually are, not just what their name claims. Introduced in v4.2.0.
First, to be clear about what this module is: the Media Library module (ddoemedialibrary) manages the media used to build CMS content pages — the files editors insert through the WYSIWYG editor (CE, PE and EE) and Visual CMS (PE and EE). It is not the product or catalog image store. It's the asset library behind your content pages, reached from the WYSIWYG/VCMS editors — nothing to do with product galleries or catalog imagery. Anything that accepts file uploads from a browser is a security surface, and a content media library — reachable by every editor — is one of the busiest. Version 4.2.0 hardens that surface: the module now validates the content of uploaded files against their declared type, rejects files whose bytes don't match their extension, scans SVGs for active content, and gives integrators a clean extension point to add their own per-format checks.
This post walks through what the module now enforces, why each check is there, and how to extend it for your own formats — without forking the module.
Why Upload Validation Matters
An extension on a filename is a claim, not a fact. A file called logo.png can contain anything — a PHP script, an HTML page with embedded JavaScript, or an SVG full of <script> tags. Historically, an upload form that trusted the extension (or only the browser-supplied MIME type, which is equally forgeable) could let a crafted file land inside the shop's media directory, where it might later be served back to an admin or a customer.
The three classic upload risks a media library has to answer for:
Version 4.2.0 addresses all three, in a layered validation chain that runs on every upload.
A Note on Scope — Read This First
The content checks are defense in depth, not a sandbox. A few boundaries are worth stating up front, before the detail:
Stated plainly so nobody over-trusts it. Within those bounds, the chain meaningfully raises the bar.
A Closer Look: the Upload Validation Chain
Every uploaded file passes through UploadedFileValidatorChainInterface. The chain is assembled from services tagged oxid_esales.media_library.validation.chain_validator, and in v4.2.0 it runs these validators in order:
Each validator throws a ValidationFailedException with a translatable message key on rejection, and the upload is refused.
1. Filename and Path Safety
FileNameValidator is the first gate. It rejects a filename outright if it:
This closes path-manipulation attempts before any file content is even read.
2. MIME-Type Validation — Does the Content Match the Name?
MimeTypeValidator resolves the upload's extension against the FileFormatRegistry. If the extension isn't a known format, the validator is a no-op (see the scope note above). For a known format, it sniffs the file's actual MIME type from its bytes and rejects the upload if that sniffed type isn't one of the type's accepted MIME types.
So a .png whose bytes are actually text/html is rejected — the extension claim and the real content disagree.
The default registry maps these extensions to their accepted MIME types:
Extension lookup is case-insensitive.
3. Content Validation — Per-Format Deep Checks
ContentValidatorChain resolves the file's format, then runs every registered content validator whose supports() returns true for that format. Two ship in the box.
RasterImageContentValidator — for jpg, jpeg, gif, png, webp, and avif, it confirms the file parses as a real image (via getimagesize()). A file that doesn't parse as a valid image is rejected, even if its extension and sniffed MIME type looked fine.
SvgContentValidator — SVG gets special treatment because it's executable XML. The validator reads the file, rejects an empty one, then hands the content to an SvgScanner that parses it as a DOM document and runs a set of violation detectors. Because SVG is XML, the parse itself is a potential XXE (XML External Entity) surface — so the scanner parses with LIBXML_NONET (network access disabled) and does not enable entity substitution, so external entities are never resolved. The upload is rejected if the SVG contains any of:
A rejected SVG is logged (with the filename and the reason) before the upload is refused, so the rejection is auditable.
Extending the Chain — Your Own Formats and Checks
This is the part integrators care about. The whole system is built on Symfony service tags, so adding support — or stricter rules — for your own file types needs no core changes and no subclassing.
Register a New Format
To make the module recognize an additional extension (so MIME validation applies to it), register a FileFormat DTO and tag it oxid_esales.media_library.validation.file_format:
services:
myvendor.media_library.format.heic:
class: OxidEsales\MediaLibrary\Validation\Format\DTO\FileFormat
arguments: ['heic', ['image/heic']]
tags: ['oxid_esales.media_library.validation.file_format']
The first argument is the (lowercased) extension; the second is the list of accepted MIME types. The registry picks it up automatically.
Add a Content Validator
To run a custom content check, implement ContentValidatorInterface and tag the service oxid_esales.media_library.validation.content_validator:
namespace MyVendor\MyModule\Validation;
use OxidEsales\MediaLibrary\Media\DataType\FilePathInterface;
use OxidEsales\MediaLibrary\Validation\Exception\ValidationFailedException;
use OxidEsales\MediaLibrary\Validation\Format\DTO\FileFormatInterface;
use OxidEsales\MediaLibrary\Validation\Validator\ContentValidator\ContentValidatorInterface;
final class HeicContentValidator implements ContentValidatorInterface {
public function supports(FileFormatInterface $format): bool { return $format->getExtension() === 'heic'; }
public function validate(FilePathInterface $filePath): void {
// inspect $filePath->getPath(); throw on rejection
if (/* not a valid HEIC */ false) { throw new ValidationFailedException('MY_MODULE_INVALID_HEIC'); }
}
}
services:
MyVendor\MyModule\Validation\HeicContentValidator:
autowire: true
tags: ['oxid_esales.media_library.validation.content_validator']
The interface is two methods:
ContentValidatorChain calls validate() on every tagged validator whose supports() matched — so several validators can cooperate on the same format.
Notes for Integrators Upgrading
Three things to check when moving an existing project to 4.2.0:
Availability
Upload content validation ships in Media Library module v4.2.0, compatible with OXID eShop compilation 7.4.x and higher (PHP 8.2+). It's active out of the box — no configuration required.
