Insecure Operation on Windows Junction / Mount Point

Description

Insecure Operation on Windows Junction / Mount Point occurs when a product opens a file or directory without properly preventing association with a junction or mount point targeting resources outside the intended control sphere. This vulnerability allows attackers to exploit Windows NTFS reparse points (junctions and mount points). A privileged program operating on a file can be tricked into reading, writing, or deleting unauthorized files if an attacker replaces the target with a hard link to a sensitive file. This enables privilege escalation and data manipulation attacks.

Risk

Junction and mount point attacks have severe implications. Privilege escalation through file manipulation. Arbitrary file read by privileged processes. Arbitrary file write or modification. Deletion of critical system files. Permanent denial of service through file deletion. Installer exploitation for code execution. Bypass of access controls. High likelihood when privileged programs operate on user-writable directories.

Solution

When designing software that will have different rights than the executor, check that files being interacted with are not improper hard links or mount points during architecture and design phase. Use Windows commands like dir /al /s /b or PowerShell's LinkType filter to detect junctions. Implement file authentication via signing. Ensure checks are atomic with file actions to avoid TOCTOU race conditions during implementation phase.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Attackers can read arbitrary files by replacing user-controlled folders with mount points and hard links.
IntegrityScope: Integrity

Arbitrary file modification possible, particularly installer rollback files that execute during reinstallation.
AvailabilityScope: Availability

Deletion of critical files when running as SYSTEM/admin can cause permanent denial of service.

Example Code

Vulnerable Code

// Vulnerable: C code without junction/mount point protection

#include <windows.h>
#include <stdio.h>

// VULNERABLE: No junction point detection
BOOL vulnerable_write_log(const char* log_dir, const char* message) {
    char log_path[MAX_PATH];
    snprintf(log_path, MAX_PATH, "%s\\app.log", log_dir);

    // VULNERABLE: Opens file without checking for reparse points
    HANDLE hFile = CreateFileA(
        log_path,
        GENERIC_WRITE,
        0,
        NULL,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );

    if (hFile == INVALID_HANDLE_VALUE) {
        return FALSE;
    }

    // Attack scenario:
    // 1. Attacker creates junction: log_dir -> C:\Windows\System32
    // 2. Privileged process writes to log_dir\app.log
    // 3. Actually writes to C:\Windows\System32\app.log
    // 4. Arbitrary file write in system directory

    DWORD written;
    WriteFile(hFile, message, strlen(message), &written, NULL);
    CloseHandle(hFile);
    return TRUE;
}

// VULNERABLE: Installer cleanup without junction protection
void vulnerable_cleanup_temp(const char* temp_dir) {
    char search_path[MAX_PATH];
    snprintf(search_path, MAX_PATH, "%s\\*", temp_dir);

    WIN32_FIND_DATAA find_data;
    HANDLE hFind = FindFirstFileA(search_path, &find_data);

    if (hFind != INVALID_HANDLE_VALUE) {
        do {
            if (strcmp(find_data.cFileName, ".") == 0 ||
                strcmp(find_data.cFileName, "..") == 0) {
                continue;
            }

            char file_path[MAX_PATH];
            snprintf(file_path, MAX_PATH, "%s\\%s", temp_dir, find_data.cFileName);

            // VULNERABLE: Deletes without checking for reparse points
            if (find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
                RemoveDirectoryA(file_path);  // May delete real directory
            } else {
                DeleteFileA(file_path);  // May delete via hard link
            }

            // Attack scenario:
            // 1. Create junction: temp_dir\subdir -> C:\Windows\System32
            // 2. Or create hard link: temp_dir\file.txt -> C:\Windows\System32\config.sys
            // 3. Privileged cleanup deletes system files

        } while (FindNextFileA(hFind, &find_data));
        FindClose(hFind);
    }
}

// VULNERABLE: Service configuration update
BOOL vulnerable_update_config(const char* user_config_dir) {
    char config_path[MAX_PATH];
    snprintf(config_path, MAX_PATH, "%s\\service.conf", user_config_dir);

    // VULNERABLE: Copies user file to system location without validation
    char system_config[] = "C:\\ProgramData\\MyService\\service.conf";

    // Attack: user_config_dir\service.conf is hard link to malicious file
    // or user_config_dir is junction to attacker-controlled location
    return CopyFileA(config_path, system_config, FALSE);
}
// Vulnerable: C# code without symlink protection

using System;
using System.IO;

public class VulnerableFileOperations
{
    // VULNERABLE: No check for reparse points
    public void VulnerableWriteData(string directory, string data)
    {
        string filePath = Path.Combine(directory, "data.txt");

        // VULNERABLE: Writes without validating target
        File.WriteAllText(filePath, data);

        // Attack: directory is junction pointing elsewhere
        // Privileged process writes to unintended location
    }

    // VULNERABLE: Directory deletion without junction check
    public void VulnerableCleanupDirectory(string path)
    {
        // VULNERABLE: Recursively deletes without checking junctions
        if (Directory.Exists(path))
        {
            Directory.Delete(path, true);  // recursive = true

            // Attack:
            // 1. Attacker creates: path\subdir -> C:\Windows\System32
            // 2. Delete(path, true) follows junction
            // 3. System32 contents deleted
        }
    }

    // VULNERABLE: Installer rollback exploitation
    public void VulnerableInstallFile(string sourcePath, string destPath)
    {
        // VULNERABLE: No verification of source location
        File.Copy(sourcePath, destPath, true);

        // Attack: sourcePath points to malicious file via hard link
        // Installer copies malicious content to protected location
    }

    // VULNERABLE: Service writes to user-controllable path
    public void VulnerableServiceLog(string userLogDir, string message)
    {
        // Service runs as SYSTEM
        string logPath = Path.Combine(userLogDir, "service.log");

        // VULNERABLE: User can redirect via junction
        using (StreamWriter sw = File.AppendText(logPath))
        {
            sw.WriteLine(message);
        }
    }
}

Fixed Code

// Fixed: C code with junction/mount point protection

#include <windows.h>
#include <stdio.h>

// FIXED: Check if path contains reparse point
BOOL is_reparse_point(const char* path) {
    DWORD attrs = GetFileAttributesA(path);
    if (attrs == INVALID_FILE_ATTRIBUTES) {
        return FALSE;
    }
    return (attrs & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
}

// FIXED: Check entire path for junctions
BOOL path_contains_junction(const char* path) {
    char temp_path[MAX_PATH];
    strncpy(temp_path, path, MAX_PATH);

    // FIXED: Check each component of the path
    char* component = temp_path;
    while (*component) {
        char* next_sep = strchr(component, '\\');
        if (next_sep) {
            *next_sep = '\0';
        }

        if (is_reparse_point(temp_path)) {
            return TRUE;  // Found junction in path
        }

        if (next_sep) {
            *next_sep = '\\';
            component = next_sep + 1;
        } else {
            break;
        }
    }

    return FALSE;
}

// FIXED: Safe file write with junction protection
BOOL safe_write_log(const char* log_dir, const char* message) {
    // FIXED: Check for junctions in path
    if (path_contains_junction(log_dir)) {
        SetLastError(ERROR_REPARSE_POINT_ENCOUNTERED);
        return FALSE;
    }

    char log_path[MAX_PATH];
    snprintf(log_path, MAX_PATH, "%s\\app.log", log_dir);

    // FIXED: Open with flag to fail on reparse points
    HANDLE hFile = CreateFileA(
        log_path,
        GENERIC_WRITE,
        0,
        NULL,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,  // Detect reparse
        NULL
    );

    if (hFile == INVALID_HANDLE_VALUE) {
        return FALSE;
    }

    // FIXED: Verify file is not a reparse point
    BY_HANDLE_FILE_INFORMATION file_info;
    if (GetFileInformationByHandle(hFile, &file_info)) {
        if (file_info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
            CloseHandle(hFile);
            SetLastError(ERROR_REPARSE_POINT_ENCOUNTERED);
            return FALSE;
        }
    }

    DWORD written;
    WriteFile(hFile, message, strlen(message), &written, NULL);
    CloseHandle(hFile);
    return TRUE;
}

// FIXED: Safe directory cleanup
void safe_cleanup_temp(const char* temp_dir) {
    // FIXED: Verify temp_dir itself is not a junction
    if (is_reparse_point(temp_dir)) {
        // FIXED: Just remove the junction, don't follow it
        RemoveDirectoryA(temp_dir);
        return;
    }

    char search_path[MAX_PATH];
    snprintf(search_path, MAX_PATH, "%s\\*", temp_dir);

    WIN32_FIND_DATAA find_data;
    HANDLE hFind = FindFirstFileA(search_path, &find_data);

    if (hFind != INVALID_HANDLE_VALUE) {
        do {
            if (strcmp(find_data.cFileName, ".") == 0 ||
                strcmp(find_data.cFileName, "..") == 0) {
                continue;
            }

            char file_path[MAX_PATH];
            snprintf(file_path, MAX_PATH, "%s\\%s", temp_dir, find_data.cFileName);

            // FIXED: Check for reparse points
            if (find_data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
                // FIXED: Remove junction without following
                RemoveDirectoryA(file_path);
                continue;
            }

            // FIXED: Check for hard links by verifying link count
            if (!(find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
                HANDLE hFile = CreateFileA(
                    file_path, 0, FILE_SHARE_READ | FILE_SHARE_WRITE,
                    NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
                );

                if (hFile != INVALID_HANDLE_VALUE) {
                    BY_HANDLE_FILE_INFORMATION info;
                    if (GetFileInformationByHandle(hFile, &info)) {
                        if (info.nNumberOfLinks > 1) {
                            // FIXED: File is hard-linked, don't delete
                            CloseHandle(hFile);
                            continue;
                        }
                    }
                    CloseHandle(hFile);
                }
            }

            if (find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
                safe_cleanup_temp(file_path);  // Recursive with same checks
                RemoveDirectoryA(file_path);
            } else {
                DeleteFileA(file_path);
            }

        } while (FindNextFileA(hFind, &find_data));
        FindClose(hFind);
    }
}

// FIXED: Safe path validation
BOOL validate_path_security(const char* path, const char* allowed_base) {
    char resolved_path[MAX_PATH];
    char resolved_base[MAX_PATH];

    // FIXED: Get canonical paths
    if (!GetFullPathNameA(path, MAX_PATH, resolved_path, NULL)) {
        return FALSE;
    }
    if (!GetFullPathNameA(allowed_base, MAX_PATH, resolved_base, NULL)) {
        return FALSE;
    }

    // FIXED: Verify path is under allowed base
    size_t base_len = strlen(resolved_base);
    if (_strnicmp(resolved_path, resolved_base, base_len) != 0) {
        return FALSE;
    }

    // FIXED: Check for junctions in path
    if (path_contains_junction(resolved_path)) {
        return FALSE;
    }

    return TRUE;
}
// Fixed: C# code with junction/mount point protection

using System;
using System.IO;
using System.Runtime.InteropServices;

public class SafeFileOperations
{
    // FIXED: Check if path is a reparse point
    private bool IsReparsePoint(string path)
    {
        FileAttributes attrs = File.GetAttributes(path);
        return (attrs & FileAttributes.ReparsePoint) != 0;
    }

    // FIXED: Check entire path for junctions
    private bool PathContainsReparsePoint(string path)
    {
        string[] components = path.Split(Path.DirectorySeparatorChar);
        string currentPath = "";

        foreach (string component in components)
        {
            if (string.IsNullOrEmpty(component)) continue;

            currentPath = string.IsNullOrEmpty(currentPath)
                ? component + Path.DirectorySeparatorChar
                : Path.Combine(currentPath, component);

            if (Directory.Exists(currentPath) && IsReparsePoint(currentPath))
            {
                return true;
            }
        }

        return false;
    }

    // FIXED: Safe file write with junction protection
    public void SafeWriteData(string directory, string data)
    {
        // FIXED: Validate path doesn't contain junctions
        if (PathContainsReparsePoint(directory))
        {
            throw new SecurityException("Path contains reparse point");
        }

        string filePath = Path.Combine(directory, "data.txt");

        // FIXED: Verify target doesn't exist as reparse point
        if (File.Exists(filePath) && IsReparsePoint(filePath))
        {
            throw new SecurityException("Target is a reparse point");
        }

        File.WriteAllText(filePath, data);
    }

    // FIXED: Safe directory deletion
    public void SafeCleanupDirectory(string path)
    {
        // FIXED: If top-level is junction, just remove the junction
        if (IsReparsePoint(path))
        {
            Directory.Delete(path, false);  // Remove junction only
            return;
        }

        // FIXED: Manually iterate without following junctions
        foreach (string file in Directory.GetFiles(path))
        {
            // FIXED: Check for hard links
            FileInfo fi = new FileInfo(file);
            if (IsHardLink(file))
            {
                // Skip hard-linked files
                continue;
            }
            File.Delete(file);
        }

        foreach (string dir in Directory.GetDirectories(path))
        {
            if (IsReparsePoint(dir))
            {
                // FIXED: Remove junction without following
                Directory.Delete(dir, false);
            }
            else
            {
                SafeCleanupDirectory(dir);  // Recursive with same checks
                Directory.Delete(dir, false);
            }
        }
    }

    // FIXED: Check if file is a hard link
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern bool GetFileInformationByHandle(
        IntPtr hFile, out BY_HANDLE_FILE_INFORMATION lpFileInformation);

    [StructLayout(LayoutKind.Sequential)]
    private struct BY_HANDLE_FILE_INFORMATION
    {
        public uint FileAttributes;
        public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
        public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
        public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
        public uint VolumeSerialNumber;
        public uint FileSizeHigh;
        public uint FileSizeLow;
        public uint NumberOfLinks;
        public uint FileIndexHigh;
        public uint FileIndexLow;
    }

    private bool IsHardLink(string path)
    {
        using (FileStream fs = File.Open(path, FileMode.Open,
                                         FileAccess.Read, FileShare.ReadWrite))
        {
            BY_HANDLE_FILE_INFORMATION info;
            if (GetFileInformationByHandle(fs.SafeFileHandle.DangerousGetHandle(), out info))
            {
                return info.NumberOfLinks > 1;
            }
        }
        return false;
    }

    // FIXED: Validate path stays within allowed directory
    public bool ValidatePathSecurity(string path, string allowedBase)
    {
        string fullPath = Path.GetFullPath(path);
        string fullBase = Path.GetFullPath(allowedBase);

        // FIXED: Ensure path is under allowed base
        if (!fullPath.StartsWith(fullBase, StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        // FIXED: Check for junctions in path
        if (PathContainsReparsePoint(fullPath))
        {
            return false;
        }

        return true;
    }
}

CVE Examples

  • CVE-2020-0668: Windows Service Tracing arbitrary file write through junction.
  • CVE-2020-1088: Windows Error Reporting privilege escalation via symlink.
  • CVE-2019-1069: Windows Task Scheduler file overwrite through hard link.

  • CWE-59: Improper Link Resolution Before File Access (parent)
  • CWE-362: Race Condition (related)
  • CWE-367: Time-of-Check Time-of-Use (TOCTOU) Race Condition (related)

References

  1. MITRE Corporation. "CWE-1386: Insecure Operation on Windows Junction / Mount Point." https://cwe.mitre.org/data/definitions/1386.html
  2. Microsoft. "Hard Links and Junctions"
  3. Project Zero. "Windows Symbolic Link Attacks"