ποΈ The Byzantine Problem: When You Can't Trust Your Own System
The name comes from the
Byzantine Empireβs complex
military structure, where
generals communicated
via unreliable messengers.
The paper was published
by Lamport, Shostak,
and Pease in 1982.
In 1982, Leslie Lamport posed a deceptively simple puzzle:
Four generals surround a city. To succeed they must all attack at the same moment, or all retreat. They can only communicate by messenger. One of the generals is a traitor. Can they still reach agreement?
This became one of the most important problems in distributed systems β and one of the most important motivations for cryptography in software. The answer is: yes, but only if the messages are signed.
This is not a theoretical curiosity. It is the exact problem your web server faces every time it processes a session token, a JWT, or a signed cookie. The traitor is not necessarily a general β it might be an attacker who has compromised one node, a tampered packet, or a man-in-the-middle sitting between your load balancer and your auth service.
The Generals Problem, Visualised
With 3f + 1 nodes you
can tolerate f Byzantine
traitors. So with 4 nodes
you survive 1 traitor.
This is the minimum bound
proven by Lamport et al.
Before code, build intuition. The simulation below lets you run the two-round broadcast protocol with and without digital signatures. Toggle the Commander as traitor and watch what happens:
The key difference: without signatures, the traitor can tell each general a different story and no one can prove it. With digital signatures, the traitorβs contradiction becomes cryptographic evidence β a general who claims to have received an order can attach the commanderβs signature, and everyone can verify it.
Why This Maps Exactly to Web Security
Byzantine failures in
web systems: any node
(server, client, proxy)
that sends conflicting
or forged messages
to different parties
to cause inconsistency.
The generals are your servers. The messengers are HTTP requests. The traitor is an attacker (or a compromised node). Here are the real-world manifestations:
JWT Algorithm Confusion
Attacker changes the header fromalg: RS256 to alg: none,
drops the signature, and the server accepts it β because it never checked.
The traitor general claiming "I have the commander's seal" on a blank document.
Session Cookie Tampering
A cookie storesrole=user without a signature or HMAC.
Attacker edits it to role=admin. Server trusts it.
The traitor relaying a forged order with no way to verify its origin.
MITM on API Calls
A proxy between your frontend and auth service intercepts and modifies messages. Without mutual TLS or signed payloads, neither side can tell. A traitor intercepting messages between two loyal generals.The JWT Attack β Live Demo
The alg: none vulnerability
was discovered in 2015 and
affected dozens of JWT
libraries. Never trust
the algorithm declared
in the header β always
enforce it server-side.
A JWT has three base64url-encoded parts: header.payload.signature.
Edit the payload below to see when the signature becomes invalid:
The signature is an HMAC-SHA256 over base64(header) + "." + base64(payload) using the
secret. Change a single byte of the payload and the signature no longer matches.
That is cryptographic integrity: the loyal generalβs message cannot be tampered with in transit.
The Real Code: PHP
β Vulnerable β unsigned session data
// β The role is stored in a cookie without any integrity check. // An attacker edits the cookie in their browser: role=admin setcookie('user', json_encode([ 'id' => 42, 'role' => 'user', ]), time() + 3600); // Later: reading it back β blindly trusted $data = json_decode($_COOKIE['user'], true); if ($data['role'] === 'admin') { // π¨ attacker is now admin }
β Secure β HMAC-signed cookie
define('COOKIE_SECRET', getenv('COOKIE_SECRET')); // 32+ byte random key function sign_cookie(array $data): string { $payload = base64_encode(json_encode($data)); $mac = hash_hmac('sha256', $payload, COOKIE_SECRET); return $payload . '.' . $mac; } function verify_cookie(string $cookie): ?array { $parts = explode('.', $cookie, 2); if (count($parts) !== 2) return null; [$payload, $mac] = $parts; $expected = hash_hmac('sha256', $payload, COOKIE_SECRET); // β constant-time comparison prevents timing attacks if (!hash_equals($expected, $mac)) { return null; // tampered β reject } return json_decode(base64_decode($payload), true); } // Usage setcookie('user', sign_cookie(['id' => 42, 'role' => 'user']), ...); $data = verify_cookie($_COOKIE['user'] ?? ''); if ($data === null) { // tampered or missing β force re-login header('Location: /login'); exit; }
β Vulnerable β JWT with alg: none
// β Trusting the algorithm from the token's own header $header = json_decode(base64_decode($parts[0]), true); $alg = $header['alg']; // attacker sends "none" if ($alg === 'none') { // skips verification entirely β π¨ auth bypass return json_decode(base64_decode($parts[1]), true); }
β Secure β enforce the algorithm server-side
function verify_jwt(string $token, string $secret): ?array { $parts = explode('.', $token); if (count($parts) !== 3) return null; [$b64h, $b64p, $b64s] = $parts; // β Never read alg from the token β always enforce HS256 $expected_sig = hash_hmac( 'sha256', $b64h . '.' . $b64p, $secret, binary: true ); $provided_sig = base64_decode( strtr($b64s, '-_', '+/') ); // β Constant-time compare if (!hash_equals($expected_sig, $provided_sig)) { return null; } $payload = json_decode(base64_decode( strtr($b64p, '-_', '+/') ), true); // β Check expiry if (($payload['exp'] ?? 0) < time()) { return null; } return $payload; }
Timing Attacks: The Subtle Byzantine
A timing attack is a
Byzantine failure where the
βtraitorβ is time itself.
If your MAC comparison
short-circuits on the
first wrong byte, an
attacker can infer the
correct MAC byte-by-byte
in ~256 Γ n requests.
There is one more Byzantine failure that pure logic cannot fix: the timing attack.
A naive string comparison like $a === $b short-circuits the moment it finds the first
differing byte. An attacker can measure response times to infer how many leading bytes
of their forged MAC are correct.
The fix is hash_equals() in PHP β which always compares every byte regardless of where
they first differ. This is called constant-time comparison and it is mandatory
for any MAC or signature check.
// β Leaks timing information if ($provided_mac === $expected_mac) { ... } // β Also leaks: early exit on mismatch if (strcmp($provided_mac, $expected_mac) === 0) { ... } // β Always runs in the same time regardless of input if (hash_equals($expected_mac, $provided_mac)) { ... } // Note: argument order matters for hash_equals β known value first!
The Full Checklist
- ✓Sign every cookie that carries authoritative data. Use
hash_hmac('sha256', $payload, $secret)and compare withhash_equals(). - ✓Never read
algfrom a JWT header. Enforce the algorithm on the server side β always. - ✓Use
hash_equals()for every MAC, signature, or token comparison. Never===orstrcmp. - ✓Store secrets in environment variables or a vault. Never hardcode them. Never commit them. Never log them.
- ✓Validate JWT
exp,iss, andaudclaims explicitly. Signed does not mean authorised. - ✓Use HTTPS everywhere β TLS ensures the channel, but it does not replace payload signing (the channel can be terminated at a proxy).
- ✓For distributed systems: require signatures on inter-service messages. A compromised internal node should not be trusted just because it is inside the VPC.
The Insight That Lamport Gave Us
In blockchain, PBFT
(Practical BFT) and its
descendants (Tendermint,
HotStuff) are what make
validators agree on
a chain despite traitors.
It all traces back to
Lamportβs 1982 paper.
The Byzantine Generals Problem sounds ancient and abstract. But its resolution β you cannot achieve reliable consensus without authenticated messages β is the foundation of every security primitive in use today:
- HMAC says: βonly someone with the shared secret can produce this signatureβ
- RSA/ECDSA says: βonly someone with the private key can produce this signatureβ
- TLS says: βonly the certificateβs true owner could have completed this handshakeβ
- JWT verification says: βonly the auth server that issued this token could have signed itβ
Every time you call hash_equals() instead of ===, every time you enforce alg: HS256
instead of trusting the header, you are implementing the lesson from 1982:
a message without a verifiable signature is worthless in an adversarial environment.
The traitor general cannot lie if every message carries a proof that only a loyal general could have created.
Further reading: Lamport, Shostak, Pease β Byzantine Generals (1982), OWASP JWT Cheat Sheet, PHP hash_equals.