mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 10:09:43 +02:00
## Summary - Adds a "Show local images" toggle in PowerToys Settings (File Explorer > Markdown) - When enabled, renders images referenced via relative paths or local file paths in the Markdown preview pane - Serves validated local image files on a WebView2 virtual host (`https://localmdimages/`) directly from the handler's resource filter - Supports local paths and UNC/network share paths - Default: OFF (preserves existing behavior) - GPO support: Admins can force-enable or force-disable via Group Policy Fixes #40787 Fixes #3713 ## Security model | Scenario | Behavior | |----------|----------| | Setting OFF (default) | All images blocked, info bar shown. Raw HTML `src` is rewritten to `#` in this state too, so a `data:` image cannot render (it is resolved internally and never reaches the resource filter) | | Setting ON + relative path (`media/img.png`) | Resolved against .md directory, rendered if under that tree | | Setting ON + path traversal (`../../secret.png`) | Blocked — resolved with `Path.GetFullPath` and checked with `Path.GetRelativePath`, including percent-encoded traversal on the serving side | | Setting ON + junction/symlink below the allowed path | Blocked — each component of the resolved path is rejected if it carries `FileAttributes.ReparsePoint`, since lexical containment alone does not prevent redirection | | Setting ON + UNC relative path (`images/pic.png` on `\\server\share`) | Allowed within share root | | Setting ON + remote URL (`https://evil.com/track.png`) | Always blocked | | data:/javascript: URI | Always blocked, in both setting states | | `srcset` on a raw HTML `<img>` | Attribute removed, in both setting states — its candidates are not validated by the `src` sanitizer | | Script execution | Always disabled (`IsScriptEnabled = false`) | | Mark-of-the-Web (MotW) | Explorer blocks preview of MotW-tagged files before our code runs (OS-level protection) | ## GPO Policy - Policy name: `MarkdownAllowLocalImages` - Registry: `HKLM\SOFTWARE\Policies\PowerToys\MarkdownAllowLocalImages` (DWORD: 1=enabled, 0=disabled) - ADMX category: **PowerToys > File Explorer Preview** - Uses `getConfiguredValue()` (individual module setting pattern, no global utility fallback) ## Screenshots ### Settings UI — new toggle _"Show local images" toggle nested under the Markdown preview section (File Explorer add-ons). Captured from a Debug build of this branch (the Settings app only runs standalone in Debug builds):_ <img width="1904" height="1014" alt="07-settings-ui-toggle" src="https://raw.githubusercontent.com/st-gr/PowerToys/pr-47857-assets/07-settings-ui-toggle.png" /> ### Settings UI — locked by GPO _With the `MarkdownAllowLocalImages` policy set to Disabled, the toggle is forced Off and grayed out, and the "managed by your organization" info bar appears:_ <img width="1904" height="1014" alt="08-settings-ui-gpo-locked" src="https://raw.githubusercontent.com/st-gr/PowerToys/pr-47857-assets/08-settings-ui-gpo-locked.png" /> ### GPO in Group Policy Editor _New "File Explorer Preview" category under PowerToys, showing the policy and its description:_ <img width="1472" height="847" alt="01-gpedit-category" src="https://github.com/user-attachments/assets/54acb539-345b-4512-9685-35930966a142" /> ### GPO set to Enabled _Policy enabled state in gpedit.msc:_ <img width="1473" height="848" alt="02-gpedit-policy-enabled" src="https://github.com/user-attachments/assets/883f6b22-179d-4311-983d-2d835e4d897a" /> ### Preview with local images rendered _Markdown preview with local image rendering enabled — relative path image renders:_ <img width="1430" height="881" alt="03-preview-images-shown" src="https://github.com/user-attachments/assets/4a7ac7f0-d57d-4feb-bf2f-7e3c9093ab52" /> ### Info bar for blocked remote images _When the document contains remote (http/https) image URLs, they are always blocked and an info bar is shown:_ <img width="1412" height="1035" alt="04-preview-infobar" src="https://github.com/user-attachments/assets/0f798bbc-ae1b-43bf-b343-394080fff8e3" /> ### GPO disabled — all images blocked _With GPO set to disabled, all images (local and remote) are blocked. Info bar reads "Some pictures have been blocked...":_ <img width="1417" height="704" alt="05-gpo-disabled-blocked" src="https://github.com/user-attachments/assets/1c7a09a7-f4b9-467c-a0c0-f670680d6a2d" /> ### Mark-of-the-Web protection _Files copied from a network source carry a Zone Identifier (MotW). Explorer blocks the preview entirely before our code runs — an OS-level security layer:_ <img width="1114" height="591" alt="06-MotW-tagged" src="https://github.com/user-attachments/assets/09cc2055-1e12-4c11-81fb-abd83ab8249c" /> ## Implementation Two layers were blocking images: 1. **Markdig AST layer** (`HTMLParsingExtension.cs`): replaced image URLs with `#` 2. **WebView2 layer** (`MarkdownPreviewHandlerControl.cs`): returned HTTP 403 for all non-HTML requests Changes: - `HTMLParsingExtension`: conditionally resolves markdown `` images to virtual host URLs with path traversal protection - `MarkdownHelper`: regex-rewrites relative `src=""` in raw HTML `<img>` tags to virtual host URLs - `MarkdownPreviewHandlerControl`: serves `https://localmdimages/` requests in the `WebResourceRequested` handler — the URL is resolved back to a file path, re-validated for containment against the allowed base path (document directory, or share root for UNC), and the bytes are returned via `CreateWebResourceResponse` with the proper content type. Note: `SetVirtualHostNameToFolderMapping` is deliberately NOT used for images — WebView2 Runtime 150+ no longer serves files from UNC/network folder mappings (verified by A/B test on 150.0.4078.48); serving from the handler works uniformly for local and UNC paths - Settings UI: new toggle nested under the Markdown preview expander, with GPO lock support - Handler `Settings.cs`: reads `EnableMdLocalImages` via `SettingsUtils`, GPO override via `GPOWrapper` - GPO: `gpo.h` individual module setting, ADMX/ADML with `FileExplorerPreview` category ## Known limitation Peek also renders Markdown through `FilePreviewCommon.MarkdownHelper`, but calls it without the local-images arguments, so **Peek does not show local images even when the setting is enabled** — it keeps the existing behavior of blocking every image. With the setting on, the same file therefore renders differently in the preview pane (images shown) and in Peek (images blocked). This is deliberate for now: wiring the setting through Peek means changing a module that is otherwise untouched by this PR. Verified that Peek itself is unaffected — it still renders Markdown correctly against the shared assembly, with images blocked as before. ## Test plan - [x] Toggle OFF: images blocked, "pictures blocked" info bar shows (existing behavior) - [x] Toggle ON with relative paths: `` renders - [x] Toggle ON with HTML img: `<img src="images/test.png">` renders - [x] Toggle ON with path traversal: `` — blocked - [x] Toggle ON with remote URL: `` — blocked, info bar shown - [x] UNC path: preview works on `\\server\share\...\file.md` with relative images - [x] UNC path with `../` within share: allowed (resolves within share root) - [x] Regression tested on WebView2 Runtime 150.0.4078.48: local + UNC images render, blocked cases (traversal, data:, remote, encoded traversal) stay blocked - [x] GPO Enabled: images forced on - [x] GPO Disabled: images forced off - [x] GPO Not Configured: user controls toggle - [x] gpedit.msc: policy appears under PowerToys > File Explorer Preview - [x] MotW-tagged files: Explorer blocks preview before our code runs - [x] Settings UI toggle locked when GPO configured (grayed out for both forced states) - [x] Other preview handlers (Monaco, SVG, PDF) unaffected 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
6.7 KiB
XML
136 lines
6.7 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<root>
|
|
<!--
|
|
Microsoft ResX Schema
|
|
|
|
Version 2.0
|
|
|
|
The primary goals of this format is to allow a simple XML format
|
|
that is mostly human readable. The generation and parsing of the
|
|
various data types are done through the TypeConverter classes
|
|
associated with the data types.
|
|
|
|
Example:
|
|
|
|
... ado.net/XML headers & schema ...
|
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
|
<resheader name="version">2.0</resheader>
|
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
|
</data>
|
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
|
<comment>This is a comment</comment>
|
|
</data>
|
|
|
|
There are any number of "resheader" rows that contain simple
|
|
name/value pairs.
|
|
|
|
Each data row contains a name, and value. The row also contains a
|
|
type or mimetype. Type corresponds to a .NET class that support
|
|
text/value conversion through the TypeConverter architecture.
|
|
Classes that don't support this are serialized and stored with the
|
|
mimetype set.
|
|
|
|
The mimetype is used for serialized objects, and tells the
|
|
ResXResourceReader how to depersist the object. This is currently not
|
|
extensible. For a given mimetype the value must be set accordingly:
|
|
|
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
|
that the ResXResourceWriter will generate, however the reader can
|
|
read any of the formats listed below.
|
|
|
|
mimetype: application/x-microsoft.net.object.binary.base64
|
|
value : The object must be serialized with
|
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
|
: and then encoded with base64 encoding.
|
|
|
|
mimetype: application/x-microsoft.net.object.soap.base64
|
|
value : The object must be serialized with
|
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
|
: and then encoded with base64 encoding.
|
|
|
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
|
value : The object must be serialized into a byte array
|
|
: using a System.ComponentModel.TypeConverter
|
|
: and then encoded with base64 encoding.
|
|
-->
|
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
|
<xsd:element name="root" msdata:IsDataSet="true">
|
|
<xsd:complexType>
|
|
<xsd:choice maxOccurs="unbounded">
|
|
<xsd:element name="metadata">
|
|
<xsd:complexType>
|
|
<xsd:sequence>
|
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
</xsd:sequence>
|
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
|
<xsd:attribute name="type" type="xsd:string" />
|
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
<xsd:attribute ref="xml:space" />
|
|
</xsd:complexType>
|
|
</xsd:element>
|
|
<xsd:element name="assembly">
|
|
<xsd:complexType>
|
|
<xsd:attribute name="alias" type="xsd:string" />
|
|
<xsd:attribute name="name" type="xsd:string" />
|
|
</xsd:complexType>
|
|
</xsd:element>
|
|
<xsd:element name="data">
|
|
<xsd:complexType>
|
|
<xsd:sequence>
|
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
</xsd:sequence>
|
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
<xsd:attribute ref="xml:space" />
|
|
</xsd:complexType>
|
|
</xsd:element>
|
|
<xsd:element name="resheader">
|
|
<xsd:complexType>
|
|
<xsd:sequence>
|
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
</xsd:sequence>
|
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
</xsd:complexType>
|
|
</xsd:element>
|
|
</xsd:choice>
|
|
</xsd:complexType>
|
|
</xsd:element>
|
|
</xsd:schema>
|
|
<resheader name="resmimetype">
|
|
<value>text/microsoft-resx</value>
|
|
</resheader>
|
|
<resheader name="version">
|
|
<value>2.0</value>
|
|
</resheader>
|
|
<resheader name="reader">
|
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
</resheader>
|
|
<resheader name="writer">
|
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
</resheader>
|
|
<data name="BlockedImageInfoText" xml:space="preserve">
|
|
<value>Some pictures have been blocked to help prevent the sender from identifying this computer. Open this item to view pictures.</value>
|
|
<comment>This text is displayed if image is blocked from being displayed.</comment>
|
|
</data>
|
|
<data name="MarkdownNotPreviewedError" xml:space="preserve">
|
|
<value>The markdown could not be preview due to an internal error.</value>
|
|
<comment>This text is displayed if markdown fails to preview</comment>
|
|
</data>
|
|
<data name="RemoteImagesBlockedInfoText" xml:space="preserve">
|
|
<value>Some online images have been blocked. Only local images from the document's folder and images on the same network share are shown.</value>
|
|
<comment>This text is displayed when local images are enabled but the document contains remote/online image URLs that were blocked.</comment>
|
|
</data>
|
|
<data name="GpoDisabledErrorText" xml:space="preserve">
|
|
<value>Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.</value>
|
|
<comment>GPO stands for the Windows Group Policy Object feature.</comment>
|
|
</data>
|
|
</root> |