Windows Shell Link (.LNK) Files: Parse IDLists, Paths, and Tracker Data Safely
Decode Windows Shell Link structures with bounded parsing, understand PIDLs and tracker data, and keep inspection separate from target resolution or launch.
A Windows shortcut with a .lnk extension is a structured Shell Link file, not merely a text file containing a target path. It can carry a shell namespace identifier list, one or more path representations, launch arguments, a working directory, display metadata, and optional tracking or environment blocks. Those fields help the Windows Shell find and launch an object, but they also make the format easy to misread when a parser assumes that every shortcut has one authoritative path.
Treat a link file as data first. Parse it without resolving or launching its target, preserve the distinction between recorded metadata and current filesystem reality, and validate every length and offset before reading. A correct offline parser and Windows’ interactive link resolver answer different questions: the first reports what the file contains; the second may search for a moved object and update the in-memory or persisted link state.
Follow the flag-controlled record sequence
The Microsoft Shell Link specification defines a sequence beginning with a fixed ShellLinkHeader, followed by optional sections selected by flags and then zero or more extra-data blocks:
SHELL_LINK = SHELL_LINK_HEADER [LINKTARGET_IDLIST] [LINKINFO]
[STRING_DATA] *EXTRA_DATA
The header is 0x4C bytes. It contains a class identifier, link flags, target-related timestamps and attributes, a file-size field, display state, and a hotkey. The flags determine whether LinkTargetIDList and LinkInfo are present and which optional StringData fields follow. Do not parse by searching for a path-like string or assuming every file uses the same optional records in the same places; advance a bounded cursor in the order specified by the flags.
StringData can describe a display name, relative path, working directory, command-line arguments, or icon location. These are separate fields with separate flags. Their presence does not guarantee that the referenced path exists, that the arguments are safe, or that the shortcut is suitable to launch. A link may also include an environment-variable block or other ExtraData structures, so “the first string is the target” is not a valid parser rule.
Keep the PIDL opaque unless you implement the namespace
When HasLinkTargetIDList is set, the header is followed by a LinkTargetIDList. Its IDListSize bounds a stored ID list, which consists of zero or more ItemID records and a zero-valued terminal ID. Each ItemIDSize includes its own size field. Check that each item is at least large enough to contain the size field, remains inside the declared list, advances the cursor, and terminates before the list boundary.
An ItemID’s data is defined by the shell data source for that namespace location. It is not necessarily a filesystem path and should not be decoded as an arbitrary NUL-terminated string. An ID list can describe namespace components that a generic offline parser does not understand. Preserve the raw bytes and report “namespace identifier list present” rather than manufacturing a path from unknown data. If an application needs shell interpretation, use the Shell API and treat resolution as a separate operation with its own policy.
Interpret LinkInfo as bounded path metadata
LinkInfo contains size fields, flags, and offsets to optional structures. Depending on those flags, it can describe a volume and local base path, a network-relative link, and a common suffix; Unicode path fields may also be available. Offsets are relative to the beginning of the LinkInfo structure and must fit inside its declared size. A malformed offset must never be allowed to escape the structure or point into a later record.
When both an ID list and a path record are present, keep both as recorded evidence instead of selecting one without applying Windows’ documented resolution rules. A local path may no longer exist, a drive letter may have changed, and a network path may be unavailable. A path string is a clue in the file, not proof of the target’s present identity. If the report’s purpose is incident response, record each representation and label whether it came from the PIDL, local path, network path, relative path, or a resolved API result.
TrackerDataBlock is a hint, not an identity assertion
Extra data is a sequence of typed blocks with size and signature fields, terminated by a terminal block. The TrackerDataBlock can include a machine name and two GUID values associated with Distributed Link Tracking. Windows may use tracking information to try to locate a target that has moved. These fields can reveal past machine or filesystem context, so treat them as potentially sensitive metadata when collecting or sharing shortcut evidence.
Do not equate tracker identifiers with a cryptographically authenticated file identity. They are data used by a resolution mechanism, not a signature over the target. Preserve them as observed values, and do not conclude that a matching or returned path is safe to execute. Even a successfully resolved object must be checked against the user-approved path, expected file identity, signing policy, and application-specific trust rules.
Use checked arithmetic for every variable-length section
An offline parser should reject truncation and impossible sizes before slicing buffers. For every read, prove that the requested bytes are within the file and the current structure. Compute offset + length with overflow-checked arithmetic; do not let a 32-bit size wrap into an apparently small bound. Verify the header size and class identifier, honor each flag’s presence rules, constrain offsets to the structure that owns them, and require IDList and extra-data terminators within their declared limits. Enforce reasonable total-file and string-size limits, then decode only after the byte range is validated.
read the complete fixed header; verify its size and format identifier
cursor = end of header
if HasLinkTargetIDList:
read IDListSize; take exactly that many bytes
parse ItemIDSize records inside that bounded slice; require TerminalID == 0
if HasLinkInfo:
read LinkInfoSize; validate all internal offsets against that slice
read each StringData field indicated by LinkFlags in specification order
read bounded ExtraData blocks until the four-byte zero terminal block
This is a parser outline, not an implementation; production code still needs exact encoding rules and handling for legal optional blocks. Test against the current [MS-SHLLINK] revision with Shell-created links, malformed and truncated records, unknown-but-well-formed extra blocks, and boundary-size values. Keep the original bytes and parser version with forensic output so another tool can reproduce the result.
Separate reading, resolving, and launching
For Windows-integrated software, IShellLinkW exposes methods to retrieve the target path, identifier list, working directory, arguments, icon, and display state. IPersistFile loads or saves the link object. Prefer these APIs for ordinary Shell behavior rather than inventing an interpretation for opaque namespace records; this still does not mean that a shortcut should be opened automatically.
IShellLink::Resolve attempts to find a moved or renamed target. Microsoft documents that it can use Distributed Link Tracking and search heuristics; depending on flags, it may show UI or update link information. For passive inventory, do not call Resolve unless the analysis explicitly requires current resolution. If it is required, run it with a clear interaction and time policy, record whether tracking and search were enabled, and avoid saving the link unless that mutation is intended.
Launching is a separate, higher-risk action. A link can specify a target and command-line arguments, and Windows can pass those arguments when the user activates it. A scanner should never use ShellExecute or another launch path just to learn what a shortcut claims to target. A deployment tool should display and validate the target, arguments, working directory, and signer before asking for approval; it should also avoid following remote or removable-media paths without an explicit policy. The icon, description, and displayed name are presentation fields, not evidence that the target is trustworthy.
Make the result auditable
For each .lnk, report the header validity, flags, present sections, raw and decoded path candidates, command-line arguments, working directory, tracker-block presence, and whether a Windows resolver was invoked. Keep “recorded in the link,” “returned by the Shell API,” and “verified on disk” as distinct statuses. That vocabulary prevents a plausible shortcut path from being mistaken for a current, authorized executable.
Shell Link’s flexibility is useful for portable shortcuts and namespace integration. A robust reader respects the protocol’s optional structure and untrusted lengths; a robust application leaves resolution and execution to explicit, separately reviewed workflows.
Related:
- CreateProcessW Command Lines: Executable Resolution and Argument Boundaries
- Windows File IDs: Compare Open Handles Without Trusting Paths
Sources:
- Microsoft Open Specifications: Shell Link (.LNK) Binary File Format
- Microsoft Open Specifications: Shell Link structures and ordering
- Microsoft Open Specifications: LinkTargetIDList and IDList size
- Microsoft Open Specifications: StringData fields and their flag-controlled order
- Microsoft Open Specifications: LinkInfo path and offset structure
- Microsoft Open Specifications: TrackerDataBlock
- Microsoft Win32: Shell Links, file contents, and resolution behavior
- Microsoft Win32: IShellLinkW interface
- Microsoft Win32: IShellLinkW::Resolve behavior and flags