A Project File Is Code: Reviewing Untrusted .csproj and MSBuild Targets Before You Open Them

A recent Iranian-nexus campaign ran malware from a custom target in a Visual Studio project before the victim compiled anything. Learn what to look for in .csproj, props and targets files, with a Python pre-open scanner and safer ways to run coding tests.

Why this matters now

On 6 October 2026, Palo Alto Networks Unit 42 published research on a campaign it calls Blinder Tunnel. It attributes the activity to an Iranian-nexus cluster, CL-STA-1178, and describes a lure aimed at a developer: a ZIP file posing as a Dubai Airports IT coding test. Inside was a Visual Studio project. According to Unit 42, a custom GetFrameworkPaths target in the .csproj copied files to %LOCALAPPDATA%\Microsoft\RuntimeBrokers and launched a payload when Visual Studio opened the project, before the developer compiled anything.

No vulnerability was needed. The build system did what build systems do. The lesson for application developers is old but easy to forget: a project file is a program, and opening it can run it. The same applies to package.json scripts, Gradle and Maven builds, Makefiles, VS Code tasks.json, .vscode/settings.json and Git hooks.

What makes a .csproj dangerous

MSBuild project files are XML, but they can contain targets, tasks and property functions. A few constructs are worth reading closely in any project you did not write.

  • <Target> elements. Particularly those with BeforeTargets, AfterTargets, InitialTargets or DefaultTargets, or whose names shadow built-in targets. Unit 42 describes the malicious target as having a plausible framework-related name. Our understanding is that IDEs invoke some targets during project load for design-time information, which is why a planted target can fire without a build. Treat any custom target as suspect until read.
  • <Exec> and <Copy> tasks. Exec runs a shell command. Copy from an odd Resources folder into a user profile path is exactly the behaviour reported.
  • <UsingTask> and inline tasks. These load or compile arbitrary code during the build.
  • Property functions. Expressions such as $([System.IO.File]::...) or $([System.Diagnostics.Process]::...) can call .NET APIs from a property definition.
  • <Import> of files outside the repo, and Directory.Build.props or Directory.Build.targets files in parent folders, which apply to every project beneath them.
  • Binary files with executable names inside the archive, such as a Resources folder holding .exe, .dll or .config files for a coding exercise that needs none.

A pre-open scanner

The script below is a starting point, not a verdict. It walks a folder you have extracted but not opened, parses MSBuild files, and flags the constructs above. It uses only the standard library. Run it before you open anything in an IDE.

#!/usr/bin/env python3
"""Flag risky constructs in MSBuild files before opening a project."""
import re
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

MSBUILD_EXT = {".csproj", ".vbproj", ".fsproj", ".props", ".targets", ".proj"}
RISKY_TASKS = {"Exec", "Copy", "Move", "Delete", "DownloadFile", "UsingTask", "WriteLinesToFile"}
RISKY_PROPFUNC = re.compile(r"\$\(\[System\.(IO|Diagnostics|Net|Reflection|Environment)", re.I)
EXEC_EXT = {".exe", ".dll", ".config", ".ps1", ".bat", ".cmd", ".vbs", ".js", ".msi"}


def local(tag: str) -> str:
    return tag.rsplit("}", 1)[-1]


def scan_msbuild(path: Path) -> list[str]:
    findings = []
    try:
        root = ET.parse(path).getroot()
    except ET.ParseError as exc:
        return [f"unparseable XML ({exc})"]
    for el in root.iter():
        name = local(el.tag)
        if name == "Target":
            attrs = {k: v for k, v in el.attrib.items()
                     if k in ("Name", "BeforeTargets", "AfterTargets", "Condition")}
            findings.append(f"custom Target {attrs}")
        if name in RISKY_TASKS:
            findings.append(f"task <{name}> {dict(el.attrib)}")
        for value in list(el.attrib.values()) + [el.text or ""]:
            if RISKY_PROPFUNC.search(value):
                findings.append(f"property function in <{name}>: {value.strip()[:80]}")
    if root.get("InitialTargets"):
        findings.append(f"InitialTargets={root.get('InitialTargets')}")
    return findings


def main(folder: str) -> int:
    base = Path(folder)
    flagged = 0
    for path in sorted(base.rglob("*")):
        if not path.is_file():
            continue
        if path.suffix.lower() in MSBUILD_EXT:
            for finding in scan_msbuild(path):
                print(f"[msbuild] {path.relative_to(base)}: {finding}")
                flagged += 1
        elif path.suffix.lower() in EXEC_EXT and "bin" not in path.parts and "obj" not in path.parts:
            print(f"[binary ] {path.relative_to(base)}: executable-type file in source tree")
            flagged += 1
    print(f"{flagged} item(s) to review by hand")
    return 1 if flagged else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "."))

Expect noise on real projects: ordinary build files legitimately contain Copy and custom targets. The point is to show what to read, not to prove malice. Anything that copies files into %LOCALAPPDATA%, %APPDATA% or a temp folder, runs a shell command, or sits next to a binary in a “Resources” folder should stop you.

Safer ways to handle untrusted projects

  1. Never open third-party projects on your main workstation. Use a disposable VM or a dev container with no access to your credentials, VPN profiles or internal network.
  2. Disconnect secrets. The exercise machine should not hold cloud credentials, SSH keys or browser sessions.
  3. Read before you load. Open the .csproj in a plain text editor first, which does not evaluate it.
  4. Verify the sender. The Blinder Tunnel pretext was a job application at a named organisation. Unit 42 states it has no evidence that Dubai Airports was breached, so the brand was used as a pretext only. Confirm any assessment through a channel you find yourself, not a link in the message.
  5. Ask recruiters for a repository link instead of a ZIP file, and read the diff history.
  6. Treat IDE trust prompts as security decisions. Where your editor offers a “trusted workspace” or restricted mode, use it for code you did not write.

Controls for engineering teams

  • Restrict where developer tools can launch processes from. A child process of devenv.exe or MSBuild.exe running from a user-profile folder is rare enough to alert on. Our SOC-focused companion article gives Sigma and KQL detections.
  • Add a policy for candidate assessments. If your own hiring includes coding tests, say publicly how they are delivered, so candidates can recognise a fake process.
  • Review Directory.Build.* files in code review. They affect every project below them, so a change there deserves the same scrutiny as a change to CI.
  • Pin and audit build-time dependencies. MSBuild SDKs and NuGet packages with build props can inject targets into your build.

Limits of this approach

A static scan cannot see targets generated at build time, fetched from a remote import, or hidden behind obfuscation. A clean scan does not make a project safe. It reduces the chance that you open an obvious trap on the machine that holds your keys. The stronger guarantee comes from isolation.

Source

The scanner is our own illustration. It has not been tested against the malicious project described by Unit 42, and we have not seen that sample.