Security

The weight of a hyphen: a lookalike domain, a self-running file, and the inspector I built to catch it

A friend's Blender community was bleeding Discord accounts, and one of his domains had started serving malware. The domain was never his, the attack rode a single missing hyphen, and the real leak was a thousand endpoints. The investigation, the file format that runs itself, and the tool I built so Blender stops asking you to trust blind. Narrative and decisions, not a playbook.

Arthur Dutra··16 min readShare ↗RSS

A friend I'll call β runs a Blender education community. Good one, the kind people pay for and then stick around in. On a Tuesday evening he messaged me with two problems wearing one coat: members of his Discord kept getting hacked, in waves, and one of his domains had started handing visitors a malware download instead of a course. He wanted to know which fire to put out first.

The thing about being the friend people call is that you inherit the mess at its worst-formatted moment, and you inherit it pre-theorized. β already had a theory. The domain got hijacked, he figured, and maybe the course platform was leaking member data, and that was feeding the Discord hacks. It was a reasonable story. It was also wrong in the specific way that matters, and the shape of how it was wrong is the whole point of this post.

I want to walk through it the way it actually unfolded, because the interesting part is not the malware. The malware was boring. The interesting part is how little it took, and how the three cheap things that did the damage map one-to-one onto three cheap things you can do about them.

It was never his domain

The first instinct, when someone says "my domain gives a malware download now," is hijack. Someone got into the registrar, or poisoned the DNS, or popped the host, and the cleanup is a credentials-and-records exercise. That instinct is usually right, and it sends you straight at the victim's own infrastructure with a bucket.

I did not touch the live host. It was reportedly serving a malware download, and you do not poke a malware host with your real fingers and your real IP to confirm what someone already told you. Everything I did in the first pass was passive: registration records, passive DNS, the network the addresses belonged to, the reverse names the addresses answered to. The unglamorous reconnaissance you can do without ever sending the hostile server a single packet that came from you.

It took about ten minutes to find the thing that reframed the entire engagement. The "compromised" domain was not β's domain. It had never been his.

His academy lived at a name with a hyphen in it. Picture northlight-academy.com. The malware lived at the same name with the hyphen removed: northlightacademy.com. Different registrar, registered by a stranger behind a privacy proxy about a year and a half earlier, delegated to a domain-parking and redirect network, answering on a budget cloud range whose reverse names openly advertised that they were parking landers. None of β's actual infrastructure had been touched. Nothing of his was breached, hijacked, or leaking. There was nothing to clean, because there was nothing of his involved.

You cannot hijack a domain that the attacker registered fresh. This was not a compromise of anything β owned. It was a forgery of his name, and the forgery was one character wide. Every visitor who lost the hyphen, every link that got retyped slightly wrong, every search result that floated the wrong one to the top, walked into a stranger's funnel. The entire attack rode the few pixels of difference between a hyphen and no hyphen.

That reframing changes the remediation completely, and I'll come back to it, because "you do not own the problem domain" is a genuinely different situation from "your domain is dirty." But first I had to answer β's other question, the one he actually cared about, which was why his community kept getting eaten.

Why a .blend is a loaded file

The members were not getting owned by browsing to a website. A parking lander that bounces you to a download is a delivery truck, not the payload. The members were getting owned by opening files, and here is the part most artists genuinely do not know, said as plainly as I can say it:

A .blend is not a document. It is a program that can choose to run itself.

Blender embeds Python for entirely legitimate reasons. Riggers automate, drivers compute, tools register themselves. A .blend can carry text blocks marked to register on load, it can carry Python driver expressions that evaluate when the scene updates, it can carry handlers that fire on open. If the "Auto Run Python Scripts" preference is enabled, all of that executes the instant the file loads. No button, no prompt, no moment where you consent to anything specific.

Blender ships with that preference off by default, and it shows a warning when a file wants to run code. That is the correct, responsible default, and I want to be fair to the project: this is not a vulnerability, it is a documented feature with a safe default. But two things conspire against the default. First, legitimate rigging content frequently needs scripts to function, so the ecosystem has spent years training artists that the way you make a fancy rig work is to flip the switch on and stop being asked. Second, and this is the real gap, the warning tells you that a file wants to run code without telling you what the code is. You are asked to make a trust decision with the single most relevant fact withheld.

Through 2025 a campaign turned that feature into a distribution channel. Publicly, the family is StealC, seeded as malicious .blend files on asset marketplaces dressed up as high-quality free models and rigs. Open the "free character rig," the embedded code runs, a small loader reaches out and pulls down an infostealer, and the infostealer goes shopping. Its list is consistent across this whole class of malware: saved browser credentials, cryptocurrency wallets, and messaging-app session tokens.

That last category is where a single infection stops being one person's bad day and becomes a community's structural problem.

The loop that eats a community

Discord is the amplifier, and the mechanism is worth understanding exactly, because it explains why β's cleanups never held.

When you log into Discord, the app stores a session token on your machine. That token is not your password. It is the key already turned in the lock. Anything holding a valid token is, as far as the platform is concerned, you, already logged in. An infostealer that lifts the token does not need your password and does not trip your two-factor, because it never logs in from scratch. It walks through a door that is already open. The only thing that invalidates a stolen token is changing the password, which rotates it. Most victims have no idea the token is the thing that matters, so they never do.

Now assemble the loop. A member opens a laced file. The stealer takes the token. The attacker rides the account, and from inside an account that the whole server already trusts, it direct-messages the rest of the members with the same bait. The next person clicks precisely because the message came from a familiar name, not a stranger. Their token gets taken. Repeat.

It is a worm with social-engineering legs, and it is self-sustaining. That is why β's server kept bleeding no matter how many times a moderator cleaned up. Every manual cleanup was racing the next trusted-looking DM, and the DM always wins, because the DM is free and the cleanup is not.

The supply line was wider than the one fake site, too. β's audience is exactly the demographic that also pirates courses, and pirated bundles are one of the oldest, healthiest malware-delivery channels there is. A pirated rig pack is an almost perfect trojan horse, because the legitimate contents of the bundle are .blend files, the exact thing that can carry the payload natively. β found his own material reposted across piracy servers, sold for a fraction of the real price. Some of those copies were, in all likelihood, carrying more than the course.

And the thing β was most afraid of, that his course platform had sprung a leak, turned out to be the one component that was completely fine. The platform is a mainstream hosted learning system. There is no exotic endpoint there to bleed member data, and a hosted platform that size is not where the weakness was. The "leak" he was imagining was real in effect and wrong in location. It was not one server bleeding a database. It was a thousand endpoints, his members' own machines, and the tokens sitting on each of them.

You cannot clean what you do not own

Here is where the reframing from the first section pays off, and where most of the actual remediation turned out to be social rather than technical, which is the part people skip because it is not fun.

You cannot disinfect a domain you do not control. There is no server of β's to wipe, no record of his to correct. The lookalike belongs to a stranger, so the only lever is to get it taken down: an abuse report to its registrar with the evidence attached, submissions to the browser safe-browsing blocklists so the warning interstitials start firing, and reports to the platforms where the bait was being posted. None of that is glamorous, and all of it is slower than you want.

The faster, higher-value move is the one that has nothing to do with infrastructure. Tell the community, in plain language, before the next person clicks. Explain the hyphen. Explain that a hijacked friend's account will DM them. Explain, specifically, that changing their Discord password is the thing that kills a stolen session, because almost nobody knows that. A pinned message that reaches a thousand members in an hour does more than any takedown that lands in a week.

And you rotate credentials under an assumption, not a finding. The honest posture is not "let me locate the breach." It is "assume at least one trusted machine in this community is already owned, and act like it." Treat the member roster as exposed, rotate the administrative and moderator accounts from a known-clean device, and stop looking for the single clean explanation. There usually isn't one. There's just a population of endpoints and a default that runs code on open.

That last clause is the one I could not stop chewing on after the immediate fire was out.

The tool that should have existed

Blender will warn you that a file wants to run code. It will not show you what the code is. You get the alarm without the photograph. You are asked to decide whether to trust a file while the one fact that would inform the decision is sitting right there inside the file, unread, because nothing offers to read it to you.

That is backwards, and it is fixable, so I fixed it. The result is a small free add-on I named BlendGuard, and the entire pitch is one sentence: see what a .blend will auto-run before you trust it. It reads a file's embedded text scripts, its Python driver expressions, its OSL script nodes, and its handlers, and it tells you what is in there and how worried to be, without executing any of it. Transparency before trust. It runs the inspection when you open a file, it can triage a file on disk before you open it at all, and it never once runs the thing it is inspecting.

I almost did not build it, for a reason worth being honest about. A ".blend malware scanner" already exists, more than once. And a scanner built on a list of known-bad strings is a treadmill you lose in slow motion, because the people writing the malware get a vote, and their vote is obfuscation. The interesting engineering problem was never detection. It was evasion.

Real payloads do not write subprocess. They write "sub"+"process". They base64-encode a blob, then base64-encode that, then decode it twice at runtime. They assemble a string out of chr() arithmetic, or reverse it, or pull it back out of a number list. A scanner that matches literal words is defeated by a first-year's worth of tricks. So BlendGuard's engine spends its effort on the unglamorous compounding work instead: it normalizes string concatenation and character escapes before it looks at anything, it decodes layered encodings recursively and re-examines what falls out, it scores the statistical randomness of the blobs it cannot decode so that an opaque packed payload still raises its hand, and it confirms the genuinely dangerous operations structurally, by parsing the code into a syntax tree, rather than trusting a regular expression to have caught them in context. Driver expressions get their own stricter track, because a slider that drives a bone has no honest reason to import a module or call a shell.

The part I want to state plainly, in the register this blog runs in, is the limit. Static analysis cannot win outright. An attacker who is willing to construct the entire payload at runtime, fetching and assembling the malicious logic only once it is already executing, walks past any static inspector, mine included. I built the thing and I will be the first to tell you what it does not do. What it does do is make the commodity ninety-five percent loud, make evasion expensive enough to deter the lazy majority, and, above all, stop lying to you the way the bare default does by omission. The primary defense remains the boring one it has always been: keep auto-run off. BlendGuard exists to make the moment you are tempted to turn it on an informed one.

I will also admit the least heroic afternoon of the build, because the unglamorous half is the half I keep coming back to on this blog. I spent a genuine chunk of time convinced my new, harder ruleset had failed to improve anything, staring at output identical to the old version, before realizing the test harness was loading a cached, compiled copy of the previous engine and had never run my new code at all. The cryptography was the easy part. The file that would not get out of its own way was the lesson.

It is free, it is open source under the GPL, and it is headed for the official Blender extension channel where it can sit one click from inside every install.

The deeper fix is provenance, not detection

Detection is the floor. It is worth building, and it is also, by its nature, a fight you can only ever draw. The ceiling is a different question entirely, and it is a much better question.

Detection asks: is this code bad? That question is undecidable in the general case and an arms race in the specific one. But there is a second question that sounds similar and is wildly easier: is this genuinely β's file, unchanged since he made it? You will never reliably answer the first. You can answer the second with arithmetic.

The shape of it is old and boring and exactly right. A publisher signs their release with a private key. Their audience trusts the matching public key once, the way you save a number from a person you already know. From then on, a file that carries a valid signature from that key, and whose contents have not been altered by a single byte, is recognized as genuine. A pirate's recompiled copy fails, not because anyone inspected it and judged it bad, but because the math no longer adds up. A stranger's upload fails because it is not signed by a key anyone agreed to trust. It flips the entire model from the losing game of chasing bad to the winnable one of recognizing good, and the recognizing-good half is the part β's community actually needed, because the question they kept failing to answer was never "is this code malicious," it was "is this really the thing my teacher made."

That piece wants to be its own thing rather than a feature welded onto a free scanner, so it is becoming one. I am calling it BlendSeal, and it is the part I am still shaping. More on it when it has earned a post of its own.

What it costs to take a community apart

Three things did the damage, and I want to be clear that not one of them was sophisticated.

A hyphen nobody was looking at. A file format that runs itself on open. A trust default that asks you to decide while hiding the thing you would decide on. That is the entire kit. No zero-day, no clever exploit, no genius on the other side, just three small gaps stacked into a funnel, plus the social physics of a community that trusts its own members by default, which is the thing that made the community good in the first place and the thing the worm used to eat it.

The fixes map onto the gaps one for one, and they are nearly as cheap. Look at the name before you trust the link. Keep auto-run off, and treat any file that asks you to turn it on as the suspect it is. And insist, before you let a file run, on either seeing what it will do or having proof of who made it. BlendGuard is the first of those. The thing I am still building is the second.

The exciting exploit is almost always the least interesting part of a story like this, and there was barely an exploit here at all. The story was never the malware. It was the hyphen, the default, and the loop, and how cheaply a room full of genuinely talented people can be turned against itself by all three at once. β's community is intact now, and the tooling exists so the next person who gets the Tuesday-evening message has something better to send back than sympathy.

That is the unglamorous half of building: most of the real safety is in the boring decisions nobody writes home about, made before anyone needed them. This is me writing home about them anyway.


A technical companion to this piece — the capability model behind BlendGuard, the four-move evasion arms race and the four answers, and the Ed25519 signing math that makes a pirated copy fail by arithmetic — is in Capabilities over signatures.