Zum Inhalt springen
Back to the blog
Development
OXID World

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

July 9, 2026
9 min read
By Rahat Hameed
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:

  • Type spoofing — a file whose real content type doesn't match its extension (an executable or HTML page wearing a .jpg name).
  • Active SVG content — SVG is XML, and XML can carry <script> elements, on* event handlers, <foreignObject>, and javascript:/data: URLs that execute when the image is opened in a browser.
  • Path manipulation — a filename containing /, \, .., or a null byte, aimed at writing outside the intended directory.
  • 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:

  • MIME-type and content validation only run for recognized formats. The module ships a registry of known extensions (images, SVG, PDF, common Office and media types — see below). If an upload's extension isn't in that registry, the MIME-type and content-validation steps are skipped for it — which is fine, because a separate step (FileExtensionValidator) governs which extensions are allowed to be uploaded at all. That allowed set is a configurable module setting, so an admin controls it. The practical rule: if you widen the set of allowed extensions, also register the new format (and, where relevant, a content validator) so the new type is actually inspected. An allowed-but-unregistered extension passes through the content layer unchecked.
  • Only images and SVG get a deep content check. The two shipped content validators cover raster images and SVG. Every other recognized format — PDF, the Office types (doc/xls/ppt), zip, and the audio/video types (mp3/avi/mpg/mpeg) — is validated by MIME-type matching only: the sniffed content type must match the extension, but the module does not parse the document's internal structure or scan it for embedded active content. Treat those types as "the bytes are the declared kind of file," not "the file is safe."
  • The raster-image check is a structural parse, not a re-encode. It rejects files that don't parse as a valid image at all; it is not a full image-sanitization pass. The SVG scan likewise detects specific classes of dangerous construct — it is a targeted denylist, not a proof of total safety.
  • Stated plainly so nobody over-trusts it. Within those bounds, the chain meaningfully raises the bar.

    A Closer Look: the Upload Validation Chain

    The upload validation chain: FileNameValidator → FileUploadStatusValidator → FileExtensionValidator → MimeTypeValidator (new) → ContentValidatorChain (new), with the built-in content validators and what each step rejects.

    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:

  • FileNameValidator — rejects empty names, names starting with a dot, names containing / or \, .. segments, or null bytes.
  • FileUploadStatusValidator — confirms the uploaded file is present on disk.
  • FileExtensionValidator — checks the extension against the set of allowed extensions, which is a configurable module setting (not a hardcoded list) — so an admin can widen or narrow what may be uploaded.
  • MimeTypeValidator — new in 4.2.0 — sniffs the real content type and compares it to the extension's declared type.
  • ContentValidatorChain — new in 4.2.0 — runs per-format content checks (image parse, SVG scan).
  • 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:

  • is empty,
  • starts with . (no dotfiles),
  • contains a path separator / or \,
  • contains a .. traversal segment, or
  • contains a null byte (\0).
  • 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:

  • jpg, jpeg: image/jpeg
  • gif: image/gif
  • png: image/png
  • webp: image/webp
  • avif: image/avif, image/avif-sequence
  • svg: image/svg+xml, image/svg, text/xml, application/xml
  • pdf: application/pdf
  • mp3: audio/mpeg
  • avi: video/x-msvideo, video/avi
  • mpg, mpeg: video/mpeg
  • doc: application/msword, application/vnd.ms-office, application/CDFV2
  • xls: application/vnd.ms-excel, application/vnd.ms-office, application/CDFV2
  • ppt: application/vnd.ms-powerpoint, application/vnd.ms-office, application/CDFV2
  • zip: application/zip, application/x-zip-compressed
  • 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:

  • <script> elements (ScriptElementDetector)
  • on* event handler attributes like onload, onclick (EventHandlerDetector)
  • <foreignObject> elements that can embed arbitrary HTML (ForeignObjectDetector)
  • javascript: or data: URLs in href / xlink:href attributes (ScriptableUrlDetector)
  • 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.

    Extending the chain in two steps: register a FileFormat DTO tagged …validation.file_format, and add a ContentValidatorInterface implementation tagged …validation.content_validator. The interface contract is two methods: supports() and validate().

    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:

  • supports(FileFormatInterface $format): bool — return true for the format(s) this validator handles.
  • validate(FilePathInterface $filePath): void — inspect the file and throw ValidationFailedException to reject it.
  • 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:

  • If you fully replace UploadedFileValidatorChainInterface, add MimeTypeValidator and ContentValidatorChain to your chain — after FileExtensionValidator — or you lose the new upload-content protection.
  • If you implement FilePathInterface yourself, add the new getExtension(): string method, returning the lowercased file extension. The new validators rely on it.
  • Check the PHP extensions the new validation depends on: ext-dom, ext-fileinfo, and ext-gd. ext-dom is new in 4.2.0 (the SVG scanner parses the DOM); ext-fileinfo backs the MIME-type sniffing and ext-gd the image handling — both were already required by earlier 4.x releases, so most environments already have them. Make sure all three are enabled.
  • 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.