Repository navigation
Resolve Windows shortcuts (.lnk) to their target file when opening - #748
mfkeeq199458 wants to merge 4 commits into
Conversation
Opening or dropping a shortcut now opens the log file it points at instead of the shortcut itself. Resolution uses the shell's own IShellLink; UNC and device targets as well as oversized files are not followed and open as regular files.
Hirogen
left a comment
There was a problem hiding this comment.
Thanks @mfkeeq199458 — nice feature, and good call to avoid IShellLink::Resolve entirely (no link tracking / network search) and to keep the target, not the .lnk, in file history. All entry paths (drop, command line, second-instance pipe, history, session restore, multi-file) go through it.
A few things before merging:
Security — UNC guard
target.StartsWith(@"\\") (ShortcutResolver.cs:95) only catches the canonical form. With SLGP_RAWPATH the target string comes straight from the .lnk bytes, so a hand-crafted link with //evil/share/x.log or \??\UNC\evil\share\x.log would get past the check and reach File.Exists, which is exactly the NetNTLM leak the comment describes. Could you normalise first (e.g. Path.GetFullPath) and then reject anything that isn't a rooted local drive path (Path.GetPathRoot is X:\, not UNC/device)? A test with forward slashes and with \\?\ / \\.\ prefixes would lock this in.
Use Vanara instead of hand-rolled COM
LogExpert.UI already references Vanara.PInvoke.Shell32, which provides ShellLink / IShellLinkW. Using it would drop the ~40 lines of [ComImport] declarations and the hand-ordered IPersistFile vtable, and match how the rest of the codebase does interop.
Path handling
SLGP_RAWPATHleaves%VAR%targets unexpanded. Please callEnvironment.ExpandEnvironmentVariables(or drop RAWPATH).- Relative targets should be resolved against the
.lnk's directory, not the working directory. - The 260-char buffer truncates long-path targets; consider a larger buffer.
Behaviour
- Shortcuts are resolved twice on the
LoadFilesWithOption/AddFileTabspath (and again inAddFileTab), so a link-to-link is followed 2 hops there but 1 hop from history. Resolving in one place would make it consistent. Array.Sortruns before resolution (:281), so multi-file order follows shortcut names rather than target names. Resolve, then sort.- When a shortcut can't be followed (UNC, broken, oversized) it currently opens the
.lnkas binary garbage with only a log line. A short message to the user would be friendlier. Not blocking, but worth considering.
Small cleanups
- The "resolve then check
.lxj" pattern is repeated three times inFileOperationService; a small helper would tidy it. COMExceptionalready derives fromExternalExceptionin the catch filter.varat:79, ausing System.Text;instead of the fully qualifiedStringBuilder, and the comments inFileOperationServicethat restate the code can go.- Tests:
CreateShortcutis duplicated across both test files, and the oversized test hardcodes1024 * 1024 + 1. Consider exposing the limit asinternaland reusing it. Please also release theWScript.ShellCOM objects.
Thanks — the UNC check and Vanara are the two I'd like to see addressed; the rest is polish.
Security: normalize the raw .lnk target (env-var expansion, resolve relative to the shortcut directory, Path.GetFullPath) before any existence probe, and only follow plain local drive targets (X:\). Disguised UNC/device forms (//host/share, \?\UNC\, \.\C:\) are now rejected, closing the NetNTLM leak via File.Exists. The guard is a pure internal SanitizeLocalTarget, unit-tested directly for the forward-slash and \?\ / \. \ forms the shell parser cannot be fed. Interop: replace hand-rolled [ComImport] declarations with Vanara's CShellLinkW/IShellLinkW, matching the rest of the codebase. Path handling: expand environment variables, resolve relative targets against the .lnk's directory, widen the GetPath buffer to 1024. Behaviour: sort after resolving so multi-file order follows target names; resolution is one hop; the repeated resolve-then-.lxj pattern in FileOperationService is consolidated into a ResolveLogFile helper. Tests: shared TestShortcuts helper with proper COM release; oversized test uses the internal MAX_SHORTCUT_SIZE; new guard tests lock in the UNC/device-path rejection.
Hirogen
left a comment
There was a problem hiding this comment.
Thanks @mfkeeq199458 — big improvement. The guard is now string-only (expand → GetFullPath against the shortcut's directory → require an X:\ root), so nothing touches the network before the check. The tests for //host/share, \\?\UNC, \??\UNC, \\.\ and %LOGONSERVER% cover exactly what I was worried about. Vanara, env-var expansion, the relative-target handling, resolve-then-sort and the ResolveLogFile helper all look good, and the new test classes pass locally (85/85).
One functional thing left, plus some polish:
Hop count (please fix)
The commit message says "resolution is one hop", but a shortcut is still resolved several times along one call chain:
- drop or command line, single file:
LoadFilesWithOptionloop (:280) →ResolveLogFile(:349) →AddFileTab(:71) = 3× SingleFiles/AskUser: pre-resolve at:280→ResolveLogFile→AddFileTab= 3×- history and session restore:
AddFileTabonly = 1×
So a link-to-link chain is followed 1 to 3 hops depending on how the file was opened, and each step loads a COM object. The simplest fix is to make Resolve idempotent: if the target itself ends in .lnk, treat it as unfollowable and return the input. Then repeated calls are cheap no-ops (they short-circuit on the extension) and the hop count is always exactly one. Then please drop the redundant call sites. Resolving in ResolveLogFile and AddFileTab (plus AddMultiFileTab) should cover every path. A test with a shortcut pointing to another shortcut would lock this in.
Small things
ShortcutResolver.cs:~86: please release theCShellLinkWin afinally(Marshal.FinalReleaseComObject), asTestShortcutsalready does.FileOperationService.cs:71-75: thestring.Equalscheck isn't needed.request = request with { FileName = ShortcutResolver.Resolve(request.FileName) };is enough.LoadFilesWithOptionnow overwrites the caller's array with resolved paths, andLoadDroppedFilesrelies on that implicitly. Fine if it stays, but it becomes moot once the extra resolves are gone.- Comments: we keep them minimal in this repo. The class remarks, the per-constant doc comments, and the NetNTLM explanation, which appears twice, could shrink to a line or two. The 7-line test class summary can go.
- Test names:
Sanitize_*→SanitizeLocalTarget_*, to matchResolve_*. - The PR description says "oversized files are not followed", but the check is on the
.lnksize, not the target. Please adjust the wording.
Mapped network drives: a target on a mapped drive (Z:\…) passes the guard, so File.Exists will contact that server. That's fine: the user mapped the drive themselves, so it's a trusted location. No change needed. A one-line comment at the root check saying this is intentional would help the next reader.
Once the hop count is consistent, I'm happy to merge. The rest is polish. Thanks!
Shortcut (.lnk) handling now follows a chain of shortcuts to its final existing local file, bounded by MAX_SHORTCUT_HOPS = 3, and the same resolution is applied at every entry point (AddFileTab, AddMultiFileTab, LoadFilesWithOption, ResolveLogFile). This corrects the earlier PR description: - resolution is no longer described as a single hop: a valid chain is followed to its end; a broken, non-local or over-limit chain keeps the last resolvable path (the original shortcut when the hop cap is hit) so it still opens as before - an oversized .lnk (above 1 MiB) is not resolved, but it is not skipped either; it is opened as the file the user selected Review follow-ups: - release the shell link COM object in a finally block - remove a redundant string.Equals - document that mapped network drives are intentionally trusted - resolve before sorting in LoadFilesWithOption so multi-file order follows the target names - trim duplicated comments - rename Sanitize_* tests to SanitizeLocalTarget_* Chain, limit and cycle behaviour is covered through an injected one-hop resolver, because the Windows shell dereferences .lnk targets when saving. All 389 LogExpert.UI tests pass.
Opening or dropping a shortcut now opens the log file it points at instead of the shortcut itself. Resolution uses the shell's own IShellLink; UNC and device targets as well as oversized files are not followed and open as regular files.