Open Bug 2042867 Opened 15 days ago Updated 1 day ago

File API Symlink Following → Arbitrary File Read

Categories

(Core :: DOM: File, defect)

defect

Tracking

()

UNCONFIRMED

People

(Reporter: dorkerdevil280, Unassigned)

References

Details

(Keywords: reporter-external, sec-other, Whiteboard: [client-bounty-form])

Attachments

(1 file)

Attached file poc_symlink_read.py

Product

Core

Component

DOM: File

Short Description

File API follows symlinks without IsSymlink() check — arbitrary file read when user selects a symlink via file picker

How did you discover this vulnerability?

Firefox version: 151.0.1
OS: macOS 26.5 (arm64)
Configuration: Default

Summary:
GetFileOrDirectoryTask::IOWork() in dom/filesystem/GetFileOrDirectoryTask.cpp does not call IsSymlink() on individually-selected files before reading their content. The path allowlist (FileSystemSecurity::ContentProcessHasAccessTo in dom/filesystem/FileSystemSecurity.cpp) uses pure string comparison via IsDescendantPath() without calling realpath() — it validates the symlink's own path, not the resolved target. The parent process then opens the file via nsIFile operations that follow symlinks by default (POSIX open() without O_NOFOLLOW).

This means a user who selects a symlink via the file picker or drag-drop unknowingly grants the web page access to the symlink's target file, which may be outside the intended access boundary.

GetDirectoryListingTask in dom/filesystem/GetDirectoryListingTask.cpp explicitly filters symlinks during directory traversal with the comment: "we allow explicit individual selection of symlinks via the file picker." This asymmetry confirms the gap was known but not mitigated for individual file selection.

Affected code — missing check (dom/filesystem/GetFileOrDirectoryTask.cpp, IOWork()):

nsresult GetFileOrDirectoryTaskParent::IOWork() {
  // ...
  bool exists;
  rv = mTargetPath->Exists(&exists);       // follows symlinks
  rv = mTargetPath->IsDirectory(&mIsDirectory); // follows symlinks
  rv = mTargetPath->IsFile(&isFile);       // follows symlinks
  // NO IsSymlink() check for individual files
  // ...
}

Correct pattern — dom/filesystem/GetDirectoryListingTask.cpp:

// GetDirectoryListingTask explicitly filters symlinks:
if (NS_WARN_IF(NS_FAILED(currFile->IsSymlink(&isLink)) || ... ) ||
    // Although we allow explicit individual selection of symlinks via the
    // file picker, we do not process symlinks in directory traversal.
    isLink || isSpecial) {
  continue;  // symlinks filtered in directory listing
}

String-only path check — dom/filesystem/FileSystemSecurity.cpp:

bool FileSystemSecurity::ContentProcessHasAccessTo(
    ContentParentId aId, const nsAString& aPath) {
  for (const auto& authorizedRoot : *paths) {
    if (IsDescendantPath(authorizedRoot, aPath)) {
      return true;  // pure string prefix match, no realpath()
    }
  }
  return false;
}

Steps to reproduce:

  1. Create a symlink on disk:

    mkdir -p ~/Desktop/ff-symlink-test
    ln -s /etc/passwd ~/Desktop/ff-symlink-test/document.pdf
    
  2. Run the PoC:

    python3 poc_symlink_read.py
    

    The script automatically:

    • Checks that the symlink exists (exits with setup instructions if not)
    • Starts Firefox 151.0.1 with --marionette --remote-allow-system-access
    • Navigates to about:blank and creates an <input type="file"> element
    • Sends the symlink path (~/Desktop/ff-symlink-test/document.pdf) to the file input via Marionette sendKeys
    • Reads the file content via JavaScript File.text()
    • Prints results and exits
  3. Observe: The File API returns 9,344 bytes of /etc/passwd content — the symlink target, not the symlink itself.

    Manual reproduction (no script needed): Open any page with <input type="file">, select ~/Desktop/ff-symlink-test/document.pdf via the file picker, and read the file content via JavaScript file.text() in the console.

Test output:

[*] Symlink: /Users/ashishkunwar/Desktop/ff-symlink-test/document.pdf -> /etc/passwd
[*] Sent file path: /Users/ashishkunwar/Desktop/ff-symlink-test/document.pdf

[*] Result: {
  "name": "document.pdf",
  "size": 9344,
  "type": "application/pdf",
  "content_first_500": "##\n# User Database\n# \n# Note that this file is consulted
    directly only when the system is running\n# in single-user mode. ...\n
    nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false\n
    root:*:0:0:System Administrator:/var/root:/bin/sh\n..."
}

[+] VULNERABLE: /etc/passwd content read through symlink!

Attack scenario:

  1. Attacker creates a seemingly innocent file that is actually a symlink:
    • invoice.pdf~/.ssh/id_rsa
    • report.docx~/Library/Keychains/login.keychain-db
    • photo.jpg → Firefox cookie database
  2. Attacker distributes the symlink (email attachment, shared folder, USB drive, AirDrop)
  3. Victim opens a malicious web page with a file upload form ("Upload your document")
  4. Victim selects the symlink via file picker — it appears as a normal file in Finder
  5. Web page reads file.text() — receives the symlink target's content
  6. Attacker exfiltrates SSH keys, browser cookies, keychain data, etc.

For drag-drop: the parent builds allowedFilePaths from nsIFile::GetPath() (nominal path without realpath()). The child's DataTransfer check passes because the symlink's own path matches the allowlist entry. The parent then reads the file following the symlink.

Impact:

  • Confidentiality: HIGH — arbitrary file read on the local system, limited only by what the user can read
  • Integrity: NONE
  • User interaction: Required (file picker selection or drag-drop)
  • Scope: Changed (crosses web-to-local boundary — web page reads local files the user did not intend to share)
  • CWE-61 (UNIX Symbolic Link Following), CWE-59 (Improper Link Resolution)
  • CVSS 3.1: 6.1 (AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N)
  • Platforms: macOS, Linux (POSIX symlink semantics). Windows not affected (no POSIX symlinks in typical user directories).

Suggested fix (Option A — add IsSymlink() check):

nsresult GetFileOrDirectoryTaskParent::IOWork() {
  // ... after Exists() check
  bool isLink = false;
  rv = mTargetPath->IsSymlink(&isLink);
  if (NS_WARN_IF(NS_FAILED(rv)) || isLink) {
    return NS_ERROR_DOM_SECURITY_ERR;
  }
  // ... rest unchanged
}

Option B — use realpath() in path validation:

bool FileSystemSecurity::ContentProcessHasAccessTo(
    ContentParentId aId, const nsAString& aPath) {
  nsCOMPtr<nsIFile> file;
  NS_NewLocalFile(aPath, true, getter_AddRefs(file));
  if (file) {
    file->Normalize();  // resolves symlinks
    nsString normalizedPath;
    file->GetPath(normalizedPath);
    // Check normalizedPath against authorized roots
  }
}

Option C — add O_NOFOLLOW to file open operations when the file was granted via picker/drag-drop path allowlist.

URL

N/A (local file system operation, not a web URL)

Attachments

  1. poc_symlink_read.py — Self-contained PoC script (Python 3, no external dependencies). Creates file input, sends symlink path, reads content. Starts and stops Firefox automatically.
  2. EVIDENCE.txt — Full test output from Firefox 151.0.1 on macOS 26.5 arm64 showing /etc/passwd content read through symlink.
Flags: sec-bounty?
Group: firefox-core-security → core-security
Component: Security → DOM: File
Product: Firefox → Core
Group: core-security → dom-core-security

The attacker can't create a symlink on your system, so it can't possibly be "arbitrary" anything.

But your own initial description quotes some of our code comments saying that symlink following was intentional, except in the directory upload case where it leads to surprises.

See bug 1813299

Andrew: confirming that this is still intentional

Flags: needinfo?(bugmail)
See Also: → CVE-2023-37206

(In reply to Daniel Veditz [:dveditz] from comment #2)

See bug 1813299

Andrew: confirming that this is still intentional

I think the explicit rationale we have comes from the options I presented in https://bugzilla.mozilla.org/show_bug.cgi?id=1813299#c15 and then your decision in https://bugzilla.mozilla.org/show_bug.cgi?id=1813299#c20.

The tl;dr for my thoughts separate from the above is:

  • On windows symlinks aren't really a thing so this doesn't really affect that many users.
  • Non-windows users (or at least linux users) are more likely to be power users who may be trying to intentionally do something clever with a symlink and there's a tough balance to strike here because it's hard to have good heuristics about what is and isn't safe and we have no real UX flow for prompting relating to files. We definitely don't have good UI avenues for indicating that we refuse to let you use the thing you just picked from the file picker.
    • In the event we were to consider implementing the File System Access API, I think we probably would need to seriously consider adding heuristics and UX in this space that we could reuse.
    • A practical concern is that if we were to start adding heuristics, we might be inundated by LLM-authored bugs about gaps in our heuristics that we weren't really invested in and only added to quiet LLMs about letting symlinks be used.
Flags: needinfo?(bugmail)
Group: dom-core-security
Keywords: sec-other
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: