diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index e60f4cc7780..577a7680504 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -76,6 +76,7 @@ IConnection ICustom IDialog IDirect +Idn IExplorer IFACEMETHOD IFile diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index f471a831251..0cddd653f4c 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -221,6 +221,7 @@ cmt cmw cmyk CNL +cnn cnt CNTRL Codeflow @@ -260,7 +261,6 @@ condrv conechokey conemu configurability -confusables conhost conime conimeinfo @@ -808,7 +808,6 @@ HIBYTE hicon HIDEWINDOW hinst -Hirots HISTORYBUFS HISTORYNODUP HISTORYSIZE @@ -825,6 +824,8 @@ HMK hmod hmodule hmon +homeglyphs +homoglyph HORZ hostable hostlib @@ -1279,7 +1280,6 @@ nullability nullness nullonfailure nullopts -NULs numlock numpad NUMSCROLL @@ -1768,7 +1768,6 @@ somefile SOURCEBRANCH sourced spammy -spand SRCCODEPAGE SRCCOPY SRCINVERT @@ -2293,7 +2292,6 @@ xunit xutr XVIRTUALSCREEN XWalk -xwwyzz xxyyzz yact YCast diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw index 4d95586e120..c94ab15d368 100644 --- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw @@ -178,6 +178,10 @@ Ctrl+Click to follow link + + Invalid URI + Whenever we encounter an invalid URI or URL we show this string as a warning. + Unable to find the selected font "{0}". diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 715939db7d7..1c1fc03da54 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -3042,56 +3042,85 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core.ClearHoveredCell(); } - winrt::fire_and_forget TermControl::_hoveredHyperlinkChanged(IInspectable /*sender*/, - IInspectable /*args*/) + // Attackers abuse Unicode characters that happen to look similar to ASCII characters. Cyrillic for instance has + // its own glyphs for а, с, е, о, р, х, and у that look practically identical to their ASCII counterparts. + // This is called an "IDN homoglyph attack". + // + // But outright showing Punycode URIs only is similarly flawed as they can end up looking similar to valid ASCII URIs. + // xn--cnn.com for instance looks confusingly similar to cnn.com, but actually represents U+407E. + // + // An optimal solution would detect any URI that contains homoglyphs and show them in their Punycode form. + // Such a detector however is not quite trivial and requires constant maintenance, which this project's + // maintainers aren't currently well equipped to handle. As such we do the next best thing and show the + // Punycode encoding side-by-side with the Unicode string for any IDN. + static winrt::hstring sanitizeURI(winrt::hstring uri) { - auto weakThis{ get_weak() }; - co_await wil::resume_foreground(Dispatcher()); - if (auto self{ weakThis.get() }) + if (uri.empty()) { - auto lastHoveredCell = _core.HoveredCell(); - if (lastHoveredCell) - { - winrt::hstring uriText = _core.HoveredUriText(); - if (uriText.empty()) - { - co_return; - } + return uri; + } - try - { - // DisplayUri will filter out non-printable characters and confusables. - Windows::Foundation::Uri parsedUri{ uriText }; - if (!parsedUri) - { - co_return; - } - uriText = parsedUri.DisplayUri(); + wchar_t punycodeBuffer[256]; + wchar_t unicodeBuffer[256]; - const auto panel = SwapChainPanel(); - const auto scale = panel.CompositionScaleX(); - const auto offset = panel.ActualOffset(); + // These functions return int, but are documented to only return positive numbers. + // Better make sure though. It allows us to pass punycodeLength right into IdnToUnicode. + const auto punycodeLength = std::max(0, IdnToAscii(0, uri.data(), gsl::narrow(uri.size()), &punycodeBuffer[0], 256)); + const auto unicodeLength = std::max(0, IdnToUnicode(0, &punycodeBuffer[0], punycodeLength, &unicodeBuffer[0], 256)); - // Update the tooltip with the URI - HoveredUri().Text(uriText); + if (punycodeLength <= 0 || unicodeLength <= 0) + { + return RS_(L"InvalidUri"); + } - // Set the border thickness so it covers the entire cell - const auto charSizeInPixels = CharacterDimensions(); - const auto htInDips = charSizeInPixels.Height / scale; - const auto wtInDips = charSizeInPixels.Width / scale; - const Thickness newThickness{ wtInDips, htInDips, 0, 0 }; - HyperlinkTooltipBorder().BorderThickness(newThickness); + const std::wstring_view punycode{ &punycodeBuffer[0], gsl::narrow_cast(punycodeLength) }; + const std::wstring_view unicode{ &unicodeBuffer[0], gsl::narrow_cast(unicodeLength) }; - // Compute the location of the top left corner of the cell in DIPS - const til::point locationInDIPs{ _toPosInDips(lastHoveredCell.Value()) }; + // IdnToAscii/IdnToUnicode return the input string as is if it's all + // plain ASCII. But we don't know if the input URI is Punycode or not. + // --> It's non-Punycode and ASCII if it round-trips. + if (uri == punycode && uri == unicode) + { + return uri; + } - // Move the border to the top left corner of the cell - OverlayCanvas().SetLeft(HyperlinkTooltipBorder(), locationInDIPs.x - offset.x); - OverlayCanvas().SetTop(HyperlinkTooltipBorder(), locationInDIPs.y - offset.y); - } - CATCH_LOG(); - } + return winrt::hstring{ fmt::format(FMT_COMPILE(L"{}\n({})"), punycode, unicode) }; + } + + void TermControl::_hoveredHyperlinkChanged(const IInspectable& /*sender*/, const IInspectable& /*args*/) + { + const auto lastHoveredCell = _core.HoveredCell(); + if (!lastHoveredCell) + { + return; } + + const auto uriText = sanitizeURI(_core.HoveredUriText()); + if (uriText.empty()) + { + return; + } + + const auto panel = SwapChainPanel(); + const auto scale = panel.CompositionScaleX(); + const auto offset = panel.ActualOffset(); + + // Update the tooltip with the URI + HoveredUri().Text(uriText); + + // Set the border thickness so it covers the entire cell + const auto charSizeInPixels = CharacterDimensions(); + const auto htInDips = charSizeInPixels.Height / scale; + const auto wtInDips = charSizeInPixels.Width / scale; + const Thickness newThickness{ wtInDips, htInDips, 0, 0 }; + HyperlinkTooltipBorder().BorderThickness(newThickness); + + // Compute the location of the top left corner of the cell in DIPS + const til::point locationInDIPs{ _toPosInDips(lastHoveredCell.Value()) }; + + // Move the border to the top left corner of the cell + OverlayCanvas().SetLeft(HyperlinkTooltipBorder(), locationInDIPs.x - offset.x); + OverlayCanvas().SetTop(HyperlinkTooltipBorder(), locationInDIPs.y - offset.y); } winrt::fire_and_forget TermControl::_updateSelectionMarkers(IInspectable /*sender*/, Control::UpdateSelectionMarkersEventArgs args) diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index d0844119231..334c9b2ddc1 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -338,7 +338,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _CurrentCursorPositionHandler(const IInspectable& sender, const CursorPositionEventArgs& eventArgs); void _FontInfoHandler(const IInspectable& sender, const FontInfoEventArgs& eventArgs); - winrt::fire_and_forget _hoveredHyperlinkChanged(IInspectable sender, IInspectable args); + void _hoveredHyperlinkChanged(const IInspectable& sender, const IInspectable& args); winrt::fire_and_forget _updateSelectionMarkers(IInspectable sender, Control::UpdateSelectionMarkersEventArgs args); void _coreFontSizeChanged(const int fontWidth,