diff --git a/src/common/FilePreviewCommon/MarkdownHelper.cs b/src/common/FilePreviewCommon/MarkdownHelper.cs
index 6573b8b5ef..2003df3340 100644
--- a/src/common/FilePreviewCommon/MarkdownHelper.cs
+++ b/src/common/FilePreviewCommon/MarkdownHelper.cs
@@ -39,7 +39,7 @@ namespace Microsoft.PowerToys.FilePreviewCommon
var softlineBreak = new Markdig.Extensions.Hardlines.SoftlineBreakAsHardlineExtension();
MarkdownPipelineBuilder pipelineBuilder;
- pipelineBuilder = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseEmojiAndSmiley().UseYamlFrontMatter().UseMathematics().DisableHtml();
+ pipelineBuilder = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseEmojiAndSmiley().UseYamlFrontMatter().UseMathematics();
pipelineBuilder.Extensions.Add(extension);
pipelineBuilder.Extensions.Add(softlineBreak);
diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/BrowserControl.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/BrowserControl.xaml
index bed3409c99..87439e2d36 100644
--- a/src/modules/peek/Peek.FilePreviewer/Controls/BrowserControl.xaml
+++ b/src/modules/peek/Peek.FilePreviewer/Controls/BrowserControl.xaml
@@ -21,6 +21,19 @@
x:Name="OpenUriDialog"
x:Uid="OpenUriDialog"
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
- SecondaryButtonClick="OpenUriDialog_SecondaryButtonClick" />
+ SecondaryButtonClick="OpenUriDialog_SecondaryButtonClick">
+
+
+
+
+
diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/BrowserControl.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/BrowserControl.xaml.cs
index e6d313d791..5a2e9900d0 100644
--- a/src/modules/peek/Peek.FilePreviewer/Controls/BrowserControl.xaml.cs
+++ b/src/modules/peek/Peek.FilePreviewer/Controls/BrowserControl.xaml.cs
@@ -34,6 +34,11 @@ namespace Peek.FilePreviewer.Controls
private Color? _originalBackgroundColor;
+ ///
+ /// URI of the current source being previewed (for resource filtering)
+ ///
+ private Uri? _currentSourceUri;
+
public delegate void NavigationCompletedHandler(WebView2? sender, CoreWebView2NavigationCompletedEventArgs? args);
public delegate void DOMContentLoadedHandler(CoreWebView2? sender, CoreWebView2DOMContentLoadedEventArgs? args);
@@ -97,6 +102,7 @@ namespace Peek.FilePreviewer.Controls
{
this.InitializeComponent();
Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", TempFolderPath.Path, EnvironmentVariableTarget.Process);
+ Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--block-new-web-contents", EnvironmentVariableTarget.Process);
}
public void Dispose()
@@ -105,6 +111,7 @@ namespace Peek.FilePreviewer.Controls
{
PreviewBrowser.CoreWebView2.DOMContentLoaded -= CoreWebView2_DOMContentLoaded;
PreviewBrowser.CoreWebView2.ContextMenuRequested -= CoreWebView2_ContextMenuRequested;
+ RemoveResourceFilter();
}
}
@@ -123,6 +130,14 @@ namespace Peek.FilePreviewer.Controls
{
/* CoreWebView2.Navigate() will always trigger a navigation even if the content/URI is the same.
* Use WebView2.Source to avoid re-navigating to the same content. */
+ _currentSourceUri = Source;
+
+ // Only apply resource filter for non-dev files
+ if (!IsDevFilePreview)
+ {
+ ApplyResourceFilter();
+ }
+
PreviewBrowser.CoreWebView2.Navigate(Source.ToString());
}
}
@@ -146,10 +161,14 @@ namespace Peek.FilePreviewer.Controls
if (IsDevFilePreview)
{
PreviewBrowser.CoreWebView2.SetVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.MonacoDirectory, CoreWebView2HostResourceAccessKind.Allow);
+
+ // Remove resource filter for dev files (Monaco needs to load resources)
+ RemoveResourceFilter();
}
else
{
PreviewBrowser.CoreWebView2.ClearVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName);
+ ApplyResourceFilter();
}
}
}
@@ -283,6 +302,66 @@ namespace Peek.FilePreviewer.Controls
}
}
+ ///
+ /// Applies strict resource filtering for non-dev files to block external resources.
+ /// This prevents XSS attacks and unwanted external content loading.
+ ///
+ private void ApplyResourceFilter()
+ {
+ // Remove existing handler to prevent duplicate subscriptions
+ RemoveResourceFilter();
+
+ // Add filter and subscribe to resource requests
+ PreviewBrowser.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All);
+ PreviewBrowser.CoreWebView2.WebResourceRequested += CoreWebView2_WebResourceRequested;
+ }
+
+ private void RemoveResourceFilter()
+ {
+ PreviewBrowser.CoreWebView2.WebResourceRequested -= CoreWebView2_WebResourceRequested;
+ }
+
+ private void CoreWebView2_WebResourceRequested(CoreWebView2 sender, CoreWebView2WebResourceRequestedEventArgs args)
+ {
+ if (_currentSourceUri == null)
+ {
+ return;
+ }
+
+ var requestUri = new Uri(args.Request.Uri);
+
+ // Allow loading the source file itself
+ if (requestUri == _currentSourceUri)
+ {
+ return;
+ }
+
+ // For local file:// resources, allow same directory and subdirectories
+ if (requestUri.Scheme == "file" && _currentSourceUri.Scheme == "file")
+ {
+ try
+ {
+ var sourceDirectory = System.IO.Path.GetDirectoryName(_currentSourceUri.LocalPath);
+ var requestPath = requestUri.LocalPath;
+
+ // Allow resources in the same directory or subdirectories
+ if (!string.IsNullOrEmpty(sourceDirectory) &&
+ requestPath.StartsWith(sourceDirectory, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+ }
+ catch
+ {
+ // If path processing fails, block for security
+ }
+ }
+
+ // Block all other resources including http(s) requests to prevent external tracking,
+ // data exfiltration, and XSS attacks
+ args.Response = PreviewBrowser.CoreWebView2.Environment.CreateWebResourceResponse(null, 403, "Forbidden", null);
+ }
+
private void CoreWebView2_DOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args)
{
// If the file being previewed is HTML or HTM, reset the background color to its original state.
@@ -344,7 +423,7 @@ namespace Peek.FilePreviewer.Controls
private async Task ShowOpenUriDialogAsync(Uri uri)
{
- OpenUriDialog.Content = uri.ToString();
+ OpenUriDialogContent.Text = uri.ToString();
var result = await OpenUriDialog.ShowAsync();
if (result == ContentDialogResult.Primary)
@@ -356,7 +435,7 @@ namespace Peek.FilePreviewer.Controls
private void OpenUriDialog_SecondaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
var dataPackage = new DataPackage();
- dataPackage.SetText(sender.Content.ToString());
+ dataPackage.SetText(OpenUriDialogContent.Text);
Clipboard.SetContent(dataPackage);
}
}
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs
index bd9fb24cb3..4516b7fc29 100644
--- a/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs
@@ -113,38 +113,41 @@ namespace Peek.FilePreviewer.Previewers
await Dispatcher.RunOnUiThread(async () =>
{
- bool isHtml = File.Extension == ".html" || File.Extension == ".htm";
- bool isMarkdown = File.Extension == ".md";
- bool isSvg = File.Extension == ".svg";
+ string extension = File.Extension;
- bool supportedByMonaco = MonacoHelper.SupportedMonacoFileTypes.Contains(File.Extension);
- bool useMonaco = supportedByMonaco && !isHtml && !isMarkdown && !isSvg;
+ // Default: non-dev file preview with standard context menu
+ IsDevFilePreview = false;
+ CustomContextMenu = false;
- IsDevFilePreview = supportedByMonaco;
- CustomContextMenu = useMonaco;
-
- if (useMonaco)
+ // Determine preview strategy based on file type priority
+ if (extension == ".md")
{
- var raw = await ReadHelper.Read(File.Path.ToString());
- Preview = new Uri(MonacoHelper.PreviewTempFile(raw, File.Extension, TempFolderPath.Path, _previewSettings.SourceCodeTryFormat, _previewSettings.SourceCodeWrapText, _previewSettings.SourceCodeStickyScroll, _previewSettings.SourceCodeFontSize, _previewSettings.SourceCodeMinimap));
- }
- else if (isMarkdown)
- {
- IsDevFilePreview = false;
+ // Markdown files use custom renderer
var raw = await ReadHelper.Read(File.Path.ToString());
Preview = new Uri(MarkdownHelper.PreviewTempFile(raw, File.Path, TempFolderPath.Path));
}
- else if (isSvg)
+ else if (extension == ".svg")
{
// SVG files are rendered directly by WebView2 for better compatibility
// with complex SVGs from Adobe Illustrator, Inkscape, etc.
- IsDevFilePreview = false;
Preview = new Uri(File.Path);
}
+ else if (extension == ".html" || extension == ".htm")
+ {
+ // Simple html file to preview. Shouldn't do things like enabling scripts or using a virtual mapped directory.
+ Preview = new Uri(File.Path);
+ }
+ else if (MonacoHelper.SupportedMonacoFileTypes.Contains(extension))
+ {
+ // Source code files use Monaco editor
+ IsDevFilePreview = true;
+ CustomContextMenu = true;
+ var raw = await ReadHelper.Read(File.Path.ToString());
+ Preview = new Uri(MonacoHelper.PreviewTempFile(raw, extension, TempFolderPath.Path, _previewSettings.SourceCodeTryFormat, _previewSettings.SourceCodeWrapText, _previewSettings.SourceCodeStickyScroll, _previewSettings.SourceCodeFontSize, _previewSettings.SourceCodeMinimap));
+ }
else
{
- // Simple html file to preview. Shouldn't do things like enabling scripts or using a virtual mapped directory.
- IsDevFilePreview = false;
+ // Fallback for other supported file types (e.g., PDF)
Preview = new Uri(File.Path);
}
});
diff --git a/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw b/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw
index f45f961268..26482bb75a 100644
--- a/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw
+++ b/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw
@@ -250,9 +250,17 @@
Dialog showed when an URI is clicked. Button to copy the URI.
- Do you want Peek to open the external application?
+ Do you want Peek to open this link?
Title of the dialog showed when an URI is clicked,"Peek" is the name of the utility.
+
+ Security Warning
+ Title of the warning banner in the open URI dialog.
+
+
+ This link will open externally. Make sure you trust the source before proceeding.
+ Warning message displayed in the banner when opening an external URI.
+
({1:N0} bytes)
Displays total number of bytes. Don't localize the "{1:N0}" part.