Security

Word documents can contain things you don't want to publish, or that could harm someone viewing the page. This page explains what word2html catches, and where its protection ends.

Summary

Found in the documentBy defaultWith --strict
Unsafe linksWarns, then convertsRefuses (exit 3)
Hidden textWarns, then convertsRefuses (exit 3)
Active contentRefuses (exit 3)Refuses (exit 3)
Malformed or hostile filesRejects (exit 1)Rejects (exit 1)

Default mode and --strict

By default, word2html prints a warning and still writes the file, including the flagged link or hidden text. That suits converting your own documents, where you'll read the warning and decide.

With --strict, the same findings become a refusal: exit code 3 and no output. All the findings are reported together, so you can fix them in one pass.

Use --strict in scripts, CI, and anywhere else nobody reads stderr. In default mode those runs exit 0 even when the HTML contains a javascript: link.

A link in a Word document can point anywhere, including at code that runs when someone clicks it. pandoc copies these into the HTML unchanged.

$ word2html links.docx
Done! Output written to: links.html
word2html: warning: unsafe link: javascript:alert(1)

$ word2html --strict links.docx
word2html: refused: unsafe link: javascript:alert(1)
word2html: error: Refusing to convert because --strict is set and the document is not clean.

word2html uses an allowlist, so anything it doesn't recognize is flagged:

AllowedExamples
Schemeshttp, https, mailto, tel, ftp
Relative linksother.html, ../up.html, //example.com/x
Links within the page#section

Everything else is flagged, including javascript:, vbscript:, data:, file:, and blob:. URLs are normalized the way browsers do it before they're checked, so disguises such as JaVaScRiPt:, java<tab>script:, and a leading space are caught too. Word's HYPERLINK fields are checked as well as ordinary links.

The check only looks at where a link points. An https link to a phishing site passes.

Hidden text

Word lets you format text as hidden, and pandoc ignores that formatting. Text the author thought was hidden appears as ordinary visible text in the HTML.

$ word2html notes.docx
Done! Output written to: notes.html
word2html: warning: 1 run(s) of text hidden in Word will be visible in the HTML (word/document.xml)

The warning gives a count and location, and never repeats the text, so it can't leak into logs. A "run" is a stretch of text with the same formatting, so one sentence can count as several.

What counts as hidden

These don't count, because Word displays the text or there's nothing to leak:

Removing hidden text

Show hidden text in Word and delete what you don't want. Word for Windows can also find it for you: File › Info › Check for Issues › Inspect Document.

Active content

Active content is anything that could run in a browser without the reader clicking anything. A crafted document can produce it, for example by labelling an HTML file as an image. Word doesn't normally create this, so it's always refused, even without --strict.

$ word2html crafted.docx
word2html: refused: active content: <embed> of data:text/html;charset=utf-8,%3Cscript%3Ealert%281%29%3C%2Fscript%3E
word2html: error: The document contains active content, so it was not converted.

This covers script, iframe, object, applet, frame, frameset, base, and inline svg elements, meta refresh, on* event handlers, and embedded media of an unexpected type.

These are allowed, because a browser won't run scripts from them:

Other protections

pandoc runs in its sandbox
word2html passes --sandbox, which restricts the files pandoc can access. This is why pandoc 3.6 is the minimum: before that, the sandbox didn't apply to embedded resources, so an image URL such as /etc/passwd could copy that file into the HTML.
No network or local file access for images
Linked images are dropped rather than fetched, so a document can't use them to track readers during conversion or pull in local files.
Filenames can't become options
pandoc always gets an absolute path with no shell involved, so a file named --lua-filter=evil.docx is just a file.
Wildcards aren't expanded
The characters [, ?, and * in a path are treated literally, so report[1].docx can't accidentally convert report1.docx.
Safe terminal output
Control characters from URLs, filenames, and pandoc messages are escaped, so a crafted link can't send escape sequences to your terminal or forge output lines.
External XML is never loaded
Any XML part with a DOCTYPE is rejected, which blocks external-entity and "billion laughs" attacks.

What isn't protected

Converting documents you don't trust

If you accept documents from other people and publish the result:

  1. Use --strict, and treat exit code 3 as "don't publish".
  2. Lower the limits, as in this recipe.
  3. Run the HTML through an HTML sanitizer before publishing. pandoc's security notes recommend this too, because it doesn't sanitize its output.
  4. Isolate the process with a container or a dedicated low-privilege user if it handles uploads on a server.
  5. Serve the output from a separate domain, with a Content Security Policy, so any script that gets through can't reach your main site.