Project

General

Profile

Actions

Defect #44358

open

Markdownized preview shows broken images: Pandoc emits <img src="media/..."> but media is never extracted

Added by Igor St 3 months ago. Updated 9 days ago.

Status:
Confirmed
Priority:
Normal
Assignee:
-
Category:
Attachments
Target version:
-
Resolution:
Affected version:

Description

The Markdown-based preview for Office attachments introduced in 7.0 (feature #8959) renders broken image icons for documents that contain images. For documents that consist mostly of images, the entire preview is a set of broken icons.

I checked for duplicates first: #8959 is the closed feature request itself, and #44029 and #44225 are related but different. I found no existing report about media handling in markdownized previews.

Steps to reproduce

  1. Install Pandoc so that markdownized previews are enabled (verified with 3.10.2)
  2. Create a .docx containing one or more images
  3. Attach it to an issue and open the attachment preview

Actual behaviour

Every image is rendered as a broken image icon, and the web server access log shows:

GET /attachments/media/image1.png  ->  404
GET /attachments/media/image2.png  ->  404

Expected behaviour

The preview should not reference resources that were never produced. Either the media is extracted and served, or the image elements are removed, ideally replaced by a short note such as "N image(s) are not shown in this preview", so that an image-only document does not look like a rendering failure.

Cause

Redmine::Markdownizer#convert builds a fixed command line in lib/redmine/markdownizer.rb:49:

args = [COMMAND, source, "-t", "gfm"]

There is no --extract-media, so the image files are never written anywhere. Pandoc does not drop the images, though: for images carrying size attributes it emits raw HTML with a relative path, for example:

<img src="media/image1.png" style="width:10.68557in;height:6.01042in" />

app/views/attachments/markdownized.html.erb renders that content through common/markup with markup_text_formatting = 'common_mark'. The CommonMark formatter allows raw HTML (unsafe: true) and then sanitizes it; the sanitizer strips dangerous attributes but keeps the img element and its src. The browser resolves the relative path against the attachment URL, which produces the 404s above.

Measured example

A 504 KB .docx containing two images produced a 147-byte preview consisting of exactly two img tags. The 100 KB output cap (markdownized_preview_max_output_size) is not involved.

No stack trace available

There is nothing to attach: no exception is raised and production.log stays completely silent. Pandoc exits 0, the preview is generated and cached successfully, and the failure is visible only in the browser and as 404s in the web server access log. That silence is part of the problem, because an administrator gets no server-side signal that previews are broken.

Not a security issue

Verified on this installation: passing <img src="media/x.png" onerror="alert(1)" /><script>alert(2)</script> through Redmine::WikiFormatting.to_html('common_mark', ...) returns <img src="media/x.png"> with the onerror attribute removed and the script tag escaped. The sanitizer works as intended; this report is only about referencing files that do not exist.

Not an installation problem

dpkg -V reports no modified files, only one pandoc binary is present, and a control run of the same document with --extract-media extracts image1.png and image2.png and rewrites src to their absolute paths. Pandoc behaves exactly as documented; the missing flag is on the Redmine side.

Possible fixes

  • Option A: run Pandoc with --extract-media into a per-attachment subdirectory of Attachment.markdownized_previews_storage_path, serve those files through AttachmentsController under the same permission checks as the attachment itself, and rewrite the src paths. New settings could bound the cost, for example markdownized_preview_max_media_count and markdownized_preview_max_media_total_size. Note that serving the extracted media as plain static files would bypass Redmine permission checks and expose attachments of private issues, so it has to go through the controller.
  • Option B, minimal: post-process the Pandoc output to drop image elements and render a short note about the omitted images.

Environment

Redmine version                7.0.0.stable.24900
Ruby version                   3.2.0-p0 (2022-12-25) [x86_64-linux]
Rails version                  8.1.3.1
Environment                    production
Database adapter               PostgreSQL
Mailer queue                   ActiveJob::QueueAdapters::AsyncAdapter
Mailer delivery                smtp
Redmine theme                  Default
SCM: Subversion 1.14.1, Git 2.34.1, Filesystem

Source: svn branches/7.0-stable, working copy r24907, last changed r24900. PostgreSQL 14.23, Pandoc 3.10.2, Passenger 6.0.24 with Apache, Ubuntu 22.04.


Files

0001-Defect-44358-Fix-images-in-markdownized-previews.patch (11.6 KB) 0001-Defect-44358-Fix-images-in-markdownized-previews.patch Florian Walchshofer, 2026-08-28 23:35
0002-Defect-44358-Add-images-to-office-document-fixtures.patch (149 KB) 0002-Defect-44358-Add-images-to-office-document-fixtures.patch Florian Walchshofer, 2026-08-28 23:35
markdownized-preview-images-placeholder.png (47.3 KB) markdownized-preview-images-placeholder.png Florian Walchshofer, 2026-08-29 22:12
markdownized-preview-images-render.png (59.1 KB) markdownized-preview-images-render.png Florian Walchshofer, 2026-08-29 22:12
0003-Defect-44358-Add-image-placeholder-mode.patch (6.56 KB) 0003-Defect-44358-Add-image-placeholder-mode.patch Florian Walchshofer, 2026-08-29 22:12
0001b-Fix-broken-images-in-markdownized-Office-document-pr.patch (12.1 KB) 0001b-Fix-broken-images-in-markdownized-Office-document-pr.patch Florian Walchshofer, 2026-10-20 23:05
0002b-Add-images-to-Office-document-fixtures-44358.patch (147 KB) 0002b-Add-images-to-Office-document-fixtures-44358.patch Florian Walchshofer, 2026-10-20 23:05
0003b-Add-image-placeholder-mode-44358.patch (6.14 KB) 0003b-Add-image-placeholder-mode-44358.patch Florian Walchshofer, 2026-10-20 23:05
0001c-Store-images-of-markdownized-previews-as-attachments.patch (19.1 KB) 0001c-Store-images-of-markdownized-previews-as-attachments.patch Florian Walchshofer, 2026-11-01 17:31
0002c-Add-images-to-Office-document-fixtures.patch (178 KB) 0002c-Add-images-to-Office-document-fixtures.patch Florian Walchshofer, 2026-11-01 17:31
Actions #1

Updated by Go MAEDA 3 months ago

  • Status changed from New to Confirmed
Actions #2

Updated by Florian Walchshofer 3 months ago

I can confirm that the issue affects DOCX, PPTX and ODT files.

For comparison, XLSX files did not generate image references, so no broken images were displayed.

Extension HTTP request Generated markdown
.docx Started GET "/attachments/media/image1.png" <img src="media/image1.png" style="width:6.69306in;height:4.92708in" />
.pptx Started GET "/attachments/ppt/media/image1.png" ![](/ppt/media/image1.png)
.odt Started GET "/attachments/Pictures/1000000000000514000003BD9FECFAD7.png" <img src="Pictures/1000000000000514000003BD9FECFAD7.png"
style="width:17cm;height:12.515cm" />

However, the root cause seems to be the same: the preview contains references to media files that are never extracted and therefore cannot be served.

Actions #3

Updated by Florian Walchshofer 2 months ago

I attached a patch for this issue.

The fix stores previews and extracted media in an attachment-specific directory and serves media through a dedicated endpoint.

Cache structure

The markdownized preview cache structure was changed from a single cache file based on digest_filesize.md to an attachment-specific directory based on the attachment id.
This change is required to support Pandoc's --extract-media option.

Previously, previews were stored as:

markdownized_previews/
└── <digest>_<filesize>.md

With image extraction enabled, the structure now becomes:

markdownized_previews/
└── 4/
    ├── preview.md
    └── media/
        ├── image1.png
        ├── image2.png
        └── image3.png

Pandoc is executed from the derived_cache directory and uses:

--extract-media=markdownized_previews/<attachment_id>/

and

chdir: basedir

This causes Pandoc to generate image references such as:

markdownized_previews/4/media/image1.png

Using an attachment-specific directory allows these generated paths to be mapped directly to the new route:

GET /attachments/markdownized_previews/4/media/image1.png

Using the previous digest_filesize cache file naming scheme would not provide a suitable target directory for extracted media files and would make it difficult to expose them through predictable URLs.

Another option would have been to post-process and rewrite the generated markdown output to replace image paths after the Pandoc conversion. However, this approach would rely on assumptions about Pandoc's generated output and media path conventions.

By using --extract-media together with an attachment-specific directory, the implementation can rely on Pandoc's generated references directly without modifying the markdown afterwards. This is more robust across different document formats (DOCX, ODT, PPTX), supports arbitrary Pandoc-generated media paths, and is less sensitive to future changes in Pandoc's output.

The cleanup logic was also updated to remove the complete preview directory, including any extracted media files, when attachments are removed or cached previews are cleared.

Configuration

Regarding the suggested markdownized_preview_max_media_count and markdownized_preview_max_media_total_size settings, I think the existing limits already provide sufficient protection.

markdownized_preview_max_source_size already limits the size of documents processed by Pandoc, including all embedded images. Since extracted media originates from the source document itself, its total size is naturally constrained by the configured source document size limit.

This patch

  • extracts embedded images using --extract-media
  • stores previews and extracted media in an attachment-specific directory
    • support arbitrary Pandoc-generated media paths, including structures such as Pictures/... (ODT), ppt/media/... (PPTX) and media/... (DOCX)
  • adds a controller action and route for serving extracted media files
  • applies the same permission checks as attachment downloads and thumbnails
  • updates preview cleanup to remove extracted media files together with the generated preview
    • DOCX previews containing embedded images
    • ODT previews containing embedded images
    • Media file delivery through the new endpoint
    • Access control checks (authorized, unauthorized and forbidden access)
    • Missing files and invalid paths

The markdownized preview now correctly displays embedded media extracted by Pandoc while preserving Redmine's existing access control rules.

Actions #4

Updated by Florian Walchshofer 2 months ago

I attached an additional patch 0003 that implements a configurable placeholder mode for embedded images.

The patch introduces a new configuration setting:

markdownized_preview_images: render
Supported values:
  • render (default)
    • extracts embedded images using Pandoc's --extract-media option
    • renders images in the generated preview
  • placeholder
    • replaces embedded images with placeholders using a Pandoc Lua filter
    • preserves image positions in the document flow
    • does not extract media files
    • displays the image's alternative text when available, otherwise a generated placeholder such as 1.png

The implementation relies on a Pandoc Lua filter and does not perform post-processing of the generated markdown output.

This option provides a lightweight alternative for installations that prefer compact previews and reduced storage usage.
It may also be useful when a quick text-focused preview is preferred over rendering embedded images.

Actions #5

Updated by Florian Walchshofer 21 days ago

I have attached updated patches for current trunk. They replace the previous patches 0001-0003.

  • 0001b-Fix-broken-images-in-markdownized-Office-document-pr.patch
    • Rebased on current trunk.
    • Stricter validation of the requested media path, with an additional test.
    • Only image files are served. Previously, the cached preview.md could also be requested.
    • preview_media is no longer accepted for API authentication, because the images are loaded by the browser with the normal session.
    • The test for deleting the preview cache is skipped when Pandoc is not available, like the other markdownized preview tests.
  • 0002b-Add-images-to-Office-document-fixtures-44358.patch
    • Unchanged (the same files as in the previous 0002), only attached again to have a complete set: msword.docx and libreoffice-writer.odt with images.
  • 0003b-Add-image-placeholder-mode-44358.patch
    • Rebased on current trunk.
    • configuration.yml.example now documents the value placeholder, which is the value the code checks. It previously documented replace.

The attachment tests pass with the default configuration.

Actions #6

Updated by Florian Walchshofer 9 days ago

I have attached a new set of patches with a different approach. They replace all previous patches.

The extracted images are now stored as regular attachments of the previewed attachment, instead of files in a cache directory.

Approach

  • Images embedded in the document are saved as attachments, with the previewed attachment as container (has_many :markdownized_preview_images).
  • The preview cache keeps its existing structure:
markdownized_previews/
└── <digest>_<filesize>.md
  • A Pandoc Lua filter writes the embedded images to a temporary directory and names them markdownized_preview_image_<n>.<ext>. The Markdown references only this filename, so it contains no attachment id.
  • The images are resolved by the existing InlineAttachmentsScrubber, like other inline images. No new route or controller action is needed, they are served by the normal attachment download.
  • The images get their permissions from the container, so they are visible to the users who can see the attachment, also after the issue is made private or moved. They cannot be edited or deleted separately, like avatars in User.
  • They are removed with the attachment by dependent: :destroy.
  • Identical images are stored once on disk by the existing digest deduplication, also across issues. Each attachment still has its own row, so that the permissions stay separate.

How a preview is displayed

1. Cached .md exists and all images it references exist for this attachment?
   yes -> display it (normal case)
   no  -> continue

2. Run Pandoc without a lock, it writes the .md to the cache
   and the images to a temporary directory

3. Lock the attachment and check the images again
   complete -> keep them, another request was faster
   missing  -> replace them with the images from the temporary directory

4. Remove the temporary directory and display the preview

The cached preview is shared by all attachments with the same digest and size, for example the copy of an issue, but each attachment has its own images. This is why step 1 also checks the images, and not only the file.

Pandoc runs outside of the transaction, because the conversion can take a long time. Requests for the same attachment can run it at the same time, but the check in step 3 ensures that a page that is already displayed does not lose its images.

--extract-media reads referenced files

While working on this I found a problem with --extract-media, which my previous patches used. Pandoc fetches every resource that a document references. With Pandoc 3.7, a crafted ODT with xlink:href="file:///etc/hostname" caused Pandoc to read that file and write it to the media directory. With Markdown input, an http:// URL was fetched too (not tested with ODT). The previous patches only serve image files, but reading the file already happens during conversion.

These patches therefore do not use --extract-media. Pandoc runs with --sandbox, and the Lua filter only looks up images in the media bag of the document, so nothing is fetched. A test fixture covers this (libreoffice-writer-linked-image.odt).

Could someone with the required permissions please remove the previous patches from this issue?.

Pandoc versions

--sandbox is available since Pandoc 2.15, older versions exit with an error on this option. Images are therefore exported only with Pandoc 2.15 or later, for all formats. The new options are used together (--sandbox --quiet --lua-filter). --quiet is needed because some distribution packages print warnings for each conversion with --sandbox (Ubuntu 26.04, Pandoc 3.7.0.2: Could not load translations).

With older versions, the conversion runs as before, without the new options and without images. This means that the broken image references of this issue remain there.

Existing cached previews

Previews cached by Redmine 7.0 reference images that were never extracted. These patches do not replace them automatically: the check for missing images only looks for the new filenames, so an old preview stays as it is until the cache is cleared. Attachment.clear_markdownized_previews removes the cached previews and now also the preview images, and they are generated again when they are displayed.

This could be done once with a migration, for example:

class ClearMarkdownizedPreviews < ActiveRecord::Migration[8.1]
  def up
    Attachment.clear_markdownized_previews
  end
end

I did not include it, because I found no existing migration that touches files. A note in doc/UPGRADING would be an alternative, please let me know what is preferred.

The same applies after upgrading Pandoc from a version below 2.15: previews that are already cached stay as they are until the cache is cleared.

Patches

The binary fixtures are in a separate patch, so that the changes can be read in the issue. The tests need both patches, and the fixture patch requires git apply.

  • 0001c-Store-images-of-markdownized-previews-as-attachments.patch: the change itself, with tests.
  • 0002c-Add-images-to-Office-document-fixtures.patch: the test fixtures with images.

The attachment tests pass with Pandoc 2.15, 3.7.0.2 and 3.13 on Ruby 4.0 and PostgreSQL 18.
With Pandoc 2.14.2 they pass too, the 10 tests for images are skipped.
A PPTX with an image was also converted correctly with Pandoc 3.13. XLSX with images is not tested.

Review and feedback are welcome, especially on storing the images as attachments and on the handling of existing cached previews.

Actions

Also available in: Atom PDF