CodeVetter · Repo Unpack · Risk map
microsoft/markitdown
The dominant risk theme is I/O breadth: the library deliberately performs filesystem, network, and subprocess I/O with the caller's privileges, and the README explicitly warns callers to sanitize inputs. Specific hotspots: unrestricted HTTP fetching (SSRF) via convert_uri, file:// URI local file reads, unbounded in-memory buffering of remote/stream content, subprocess invocation of exiftool, plugin code execution via entry points, and an unauthenticated MCP HTTP mode. Defusedxml and an exiftool version check show some hardening is present.
- convert_uri fetches arbitrary http(s) URLs with the process's requests session and no allowlist — the README's Security Considerations section tells integrators to block private/loopback/link-local/metadata-service addresses themselves; nothing in code enforces this.
- file:// URIs resolve to local paths and convert_local opens them; file_uri_to_path rejects non-localhost netlocs and UNC/device paths but permits any absolute local path — the MCP tool exposes this to remote clients in HTTP mode.
- The MCP server's --http mode has NO authentication; the code itself prints a warning when --host is non-loopback stating any reachable user 'can read files and fetch network resources accessible to this user'.
- Remote and stream inputs are buffered fully into memory: convert_response iter_content into BytesIO and convert_stream buffers non-seekable streams — a hostile or huge response/stream can exhaust memory.
- exiftool_metadata shells out to subprocess.run([exiftool_path, ...]); the exiftool path is resolved from the EXIFTOOL_PATH env var, kwargs, or a PATH search restricted to well-known directories, and a version gate rejects exiftool <12.24 citing CVE-2021-22204.
- Plugin loading executes arbitrary installed Python code: entry_points(group='markitdown.plugin').load() then register_converters(); failures are only warned about. Plugins are off by default but -p/--use-plugins or enable_plugins=True activates all of them.
- The _convert loop asserts (not raises) on stream-position invariants ('assert cur_pos == file_stream.tell()') — running under python -O disables these checks, silently removing the safety net that protects converters from a misbehaving accepts().
- Each MCP convert_to_markdown call constructs a brand-new MarkItDown() — re-registering ~17 converters and re-instantiating magika.Magika() (a model load) per request, which is a latency/CPU cost on every tool call. (inferred)
- BeautifulSoup 'html.parser' and defusedxml are used for untrusted markup/XML (EPUB container/OPF via defusedxml.minidom), but docx pre-processing parses document.xml with BeautifulSoup and xml.etree.ElementTree — XML bomb surface depends on those parsers' defaults rather than defusedxml. (inferred)
- HtmlConverter's markdownify path can hit RecursionError on deeply nested HTML; it falls back to get_text() unless a 'strict' kwarg is set — acceptable degradation but callers get silently different output shape.
- ZipConverter swallows FileConversionException/UnsupportedFormatException per member and the archive is read via zipObj.read(entry) fully into memory per entry; nested zips recurse through the same converter stack without a depth limit visible in this file.
- Error detail asymmetry: FileConversionException aggregates per-converter exception messages (which can include SDK/internal detail) while the MCP layer deliberately sanitizes to generic ToolError strings — good for the MCP boundary, but library callers receive verbose internals.