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 document | By default | With --strict |
|---|---|---|
| Unsafe links | Warns, then converts | Refuses (exit 3) |
| Hidden text | Warns, then converts | Refuses (exit 3) |
| Active content | Refuses (exit 3) | Refuses (exit 3) |
| Malformed or hostile files | Rejects (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.
Unsafe links
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:
| Allowed | Examples |
|---|---|
| Schemes | http, https, mailto, tel, ftp |
| Relative links | other.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
- Text in the document body, footnotes, or endnotes, which are the parts pandoc publishes
- Hidden through direct formatting, a character style, a paragraph style, a style it's based on, or the document defaults
These don't count, because Word displays the text or there's nothing to leak:
- Formatting explicitly turned off, such as direct formatting that overrides a hiding style
- Text hidden by both a paragraph style and a character style, since these cancel out in Word
- A hidden paragraph mark on otherwise visible text
- Text hidden only in Web Layout view, which is common in tables of contents
- Blank text
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:
- Images in
<img>, including SVG files - PDFs in
<embed> - Video and audio
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/passwdcould 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.docxis just a file. - Wildcards aren't expanded
- The characters
[,?, and*in a path are treated literally, soreport[1].docxcan't accidentally convertreport1.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
DOCTYPEis rejected, which blocks external-entity and "billion laughs" attacks.
What isn't protected
- Misleading content. Phishing links, false claims, and link text that doesn't match the destination all pass.
- Sensitive visible text. Nothing looks for passwords or personal data, and tracked insertions are published.
- A full guarantee against harmful HTML. The checks target known risks, and they aren't a complete HTML sanitizer.
- Resource use up to the limits. A small hostile file can still use memory up to the configured limit, and run until the time limit, before it's stopped.
- Bugs in pandoc itself. Keep pandoc up to date.
Converting documents you don't trust
If you accept documents from other people and publish the result:
- Use
--strict, and treat exit code 3 as "don't publish". - Lower the limits, as in this recipe.
- Run the HTML through an HTML sanitizer before publishing. pandoc's security notes recommend this too, because it doesn't sanitize its output.
- Isolate the process with a container or a dedicated low-privilege user if it handles uploads on a server.
- Serve the output from a separate domain, with a Content Security Policy, so any script that gets through can't reach your main site.