FlawOpen

flawopen.com/Incidents/windows-update-stack-symlink-eop-september-2026

● Microsoft · CVE-2026-81963 · September 2026 · CVSS 7.8 · High
FlawOpen Security Research

CVE-2026-81963: Windows Update Symlink Flaw

CVE-2026-81963 is an exploited Windows Update link-resolution flaw. See affected builds, Microsoft’s fix, and safe service file handling.[1], [2]

💡 Plain English Explainer (ELI5)

A night porter with a master key delivers parcels to "the storeroom at the end of the corridor", following the arrows painted on the walls without checking where he ends up. Tenants are allowed to move those arrows. One tenant turns the storeroom arrow to point at the manager's office, and the porter, whose key opens every door, walks in and leaves the parcel on the manager's desk. The tenant could never open that door; the porter's key did it for them. The fix is for the porter to read the number on the door he actually opened, and to stop following arrows that tenants can move.[5], [9]

Core Concepts & Subsystem Terms

Windows Update Stack
The Windows components that scan for, download, stage and install updates. They run as SYSTEM because installing updates means writing to protected parts of the operating system.[1]
SYSTEM (NT AUTHORITY\SYSTEM)
The local account Windows services run as. It has more rights than an administrator on the local machine, so any file operation it performs is almost never refused.[1]
Junction / symbolic link (reparse point)
NTFS objects that make one path resolve to another. A standard user can create a junction in a folder they control, redirecting that folder name to a directory they cannot write to.[9]
Link following (CWE-59)
A program opens, writes, moves or deletes a file by name without checking that the name resolves where it expects, so a link planted by someone else redirects the operation.[5], [2]
FILE_FLAG_OPEN_REPARSE_POINT
A CreateFileW flag that opens the final path component itself when it is a reparse point, rather than following that final link. It does not by itself prevent reparse processing in earlier path components.[6]
TOCTOU (CWE-367)
Time-of-check to time-of-use: the file system changes between a check and the operation that relies on it. Checking a path string and then opening it by name leaves such a window.[12]
CWE-284 (Improper Access Control)
Microsoft assigns both CWE-59 and CWE-284 to this CVE. CWE-59 names the specific link-resolution weakness. CWE-284 is a broad Pillar CWE that MITRE explicitly discourages for mapping real-world vulnerabilities. Treat it as Microsoft’s broader classification of this same reported issue, not evidence of a second bug.[2], [11], [5]

Framework mappings

Incident Timeline

2026-08-27

Microsoft reserves CVE-2026-81963.[2]

2026-09-08

September Patch Tuesday: Microsoft publishes the fix and marks the flaw "Exploitation Detected" and not publicly disclosed.[1]

2026-09-08

CISA adds CVE-2026-81963 to the Known Exploited Vulnerabilities catalog, with a remediation due date of 22 September 2026 for US federal agencies.[4]

2026-09-15

Microsoft publishes a revision of the advisory.[1]

Step-by-Step Attack Flow

Step 1

A SYSTEM service works in a path users can change

Update Stack components run as SYSTEM and handle files in staging and working directories. If a standard user can create, delete or rename entries anywhere along such a path, they decide where the path resolves. Microsoft has not said which path was affected in this case.[1]

Step 2

The path is redirected

A junction or symbolic link makes a directory name resolve somewhere else, such as a protected system folder. Creating a junction in a folder the user controls needs no administrator rights.[5], [9]

Step 3

The service follows the link

Windows normally resolves reparse points while opening a path. FILE_FLAG_OPEN_REPARSE_POINT controls the final component; earlier components can still be resolved. If a service then writes through an unvalidated path, the access check uses its SYSTEM token. Validate the directory handle and use handle-relative creation for the child file.[6], [8]

Step 4

A protected location is changed

The service creates, overwrites, moves or deletes a file somewhere only SYSTEM or administrators should control. Microsoft's FAQ confirms that exploitation gives the attacker SYSTEM privileges.[1], [2]

Source Code: Flaw vs. Secure Implementation

Microsoft has not published the affected function or patch. This defensive example is not Microsoft's code. Checking only the owner is insufficient because an object's owner does not prove that its DACL denies standard-user write access. This pattern assumes the installer creates the fixed staging path and every parent with an explicit SYSTEM/Administrators-only write DACL. The function does not read or validate the DACL; installer-provisioned permissions are a deployment precondition. It validates the opened directory handle, then creates a single child name relative to that handle with NtCreateFile and FILE_CREATE; it never reopens a concatenated path. Test the ACL and filesystem behavior on supported deployments. Install Microsoft's update to remediate CVE-2026-81963.[5], [6], [7], [8], [9], [10]

Source excerpt: Illustrative Windows path-handling pattern; not Microsoft source

UNPATCHED FLAW
/* Illustrative pattern, not Microsoft's source: Microsoft has not published
   the affected function. A service running as SYSTEM writes a file into a
   staging directory that standard users can modify. */
BOOL WriteStagedFile(LPCWSTR stagingDir, LPCWSTR name,
                     const BYTE *data, DWORD size)
{
    WCHAR path[MAX_PATH];
    if (FAILED(StringCchPrintfW(path, MAX_PATH, L"%s\\%s", stagingDir, name)))
        return FALSE;

    /* CWE-59: CreateFileW resolves junctions and symbolic links in every
       component of the path, and the access check uses the service's
       SYSTEM token. If a user can swap stagingDir for a junction, this
       write lands wherever the junction points. */
    HANDLE h = CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
                           FILE_ATTRIBUTE_NORMAL, NULL);
    if (h == INVALID_HANDLE_VALUE)
        return FALSE;

    DWORD written = 0;
    BOOL ok = WriteFile(h, data, size, &written, NULL) && written == size;
    CloseHandle(h);
    return ok;
}
HARDENED SECURE PATCH
/* Illustrative defensive pattern only; not Microsoft's source or patch.
   The installer must provision this path and every parent with an explicit
   DACL that allows writes only to SYSTEM and Administrators. */
#include <windows.h>
#include <winternl.h>

static const WCHAR STAGING_DIR[] =
    L"C:\\ProgramData\\Vendor\\Update\\Staging";

BOOL WriteStagedFile(LPCWSTR name, const BYTE *data, DWORD size)
{
    if (name == NULL || data == NULL || name[0] == L'\0' ||
        wcspbrk(name, L"\\/:") != NULL || wcscmp(name, L".") == 0 ||
        wcscmp(name, L"..") == 0)
        return FALSE;

    /* Pin the directory object; omitting FILE_SHARE_DELETE blocks rename. */
    HANDLE dir = CreateFileW(STAGING_DIR,
                             FILE_LIST_DIRECTORY | FILE_TRAVERSE | FILE_READ_ATTRIBUTES,
                             FILE_SHARE_READ | FILE_SHARE_WRITE,
                             NULL, OPEN_EXISTING,
                             FILE_FLAG_BACKUP_SEMANTICS |
                                 FILE_FLAG_OPEN_REPARSE_POINT,
                             NULL);
    if (dir == INVALID_HANDLE_VALUE)
        return FALSE;

    FILE_ATTRIBUTE_TAG_INFO tag;
    WCHAR actual[MAX_PATH];
    BOOL ok = FALSE;
    DWORD n;
    if (!GetFileInformationByHandleEx(dir, FileAttributeTagInfo,
                                      &tag, sizeof tag) ||
        (tag.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT))
        goto done;

    n = GetFinalPathNameByHandleW(dir, actual, MAX_PATH,
                                  FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
    if (n == 0 || n >= MAX_PATH ||
        _wcsicmp(actual, L"\\\\?\\C:\\ProgramData\\Vendor\\Update\\Staging") != 0)
        goto done;

    /* Open the new child relative to this handle, never by rebuilding a path. */
    size_t nameChars = wcslen(name);
    if (nameChars == 0 || nameChars > 255)
        goto done;
    USHORT nameBytes = (USHORT)(nameChars * sizeof(WCHAR));
    UNICODE_STRING relativeName = { nameBytes, nameBytes, (PWSTR)name };
    OBJECT_ATTRIBUTES oa = {0};
    oa.Length = sizeof oa;
    oa.RootDirectory = dir;
    oa.ObjectName = &relativeName;
    oa.Attributes = OBJ_CASE_INSENSITIVE;

    IO_STATUS_BLOCK iosb = {0};
    HANDLE file = INVALID_HANDLE_VALUE;
    NTSTATUS status = NtCreateFile(
        &file, FILE_WRITE_DATA | SYNCHRONIZE, &oa, &iosb, NULL,
        FILE_ATTRIBUTE_NORMAL, 0, FILE_CREATE,
        FILE_NON_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT, NULL, 0);
    if (!NT_SUCCESS(status))
        goto done; /* FILE_CREATE fails if an entry already exists. */

    DWORD written = 0;
    ok = WriteFile(file, data, size, &written, NULL) && written == size;
    CloseHandle(file);
done:
    CloseHandle(dir);
    return ok;
}

Engineering & System Hardening Checklist

Sources