Vulnerability 5 & Bonus: Replay Attack
OWASP A08:2021 — Software and Data Integrity Failures
Severity: Medium — Silent Data Corruption
What Is a Replay Attack?
An attacker intercepts a valid, authenticated request and re-sends it later. Even though the attacker can't read the contents (thanks to encryption) or forge new requests (thanks to authentication), they can still cause damage by replaying a captured operation.
The Classic Scenario
Time 0: Alice uploads "config.txt" version 1 (Attacker captures this)
Time 5: Alice updates "config.txt" to version 2
Time 10: Attacker replays the captured version 1 upload
→ config.txt silently reverts to version 1
→ Alice doesn't know her changes were lost
The attacker didn't need to:
- Know the file contents
- Know Alice's password
- Create a valid session token
They just needed to record and replay a single network packet.
Vulnerable Code
File: vulnerable/src/server/ReplicaNode.java
// VULNERABILITY 5: No replay protection — no nonce, no timestamp.
// The same operation can be replayed indefinitely.
@Override
public String upload(String token, String filename, byte[] data) throws RemoteException {
// No nonce check. No timestamp check.
// If an attacker replays this call, the server processes it again.
saveFile(filename, data);
multicastOperation("UPLOAD", filename);
return "File uploaded successfully";
}
@Override
public String delete(String token, String filename) throws RemoteException {
// Same problem — replayable
deleteFileLocally(filename);
multicastOperation("DELETE", filename);
return "File deleted";
}
Attack Demo
Attacker5_ReplayAttack
cd vulnerable
# (nodes must be running)
java -cp out attacker.Attacker5_ReplayAttack
The attacker's strategy:
// Phase 1: Legitimate operations (Alice working normally)
FileSystemService service = /* connect */;
// Upload v1 (attacker captures this RMI call)
service.upload("alice_token", "config.txt", "Version 1 content".getBytes());
System.out.println("Uploaded v1 — attacker captured this call");
// Update to v2
service.upload("alice_token", "config.txt", "Version 2 — important changes".getBytes());
System.out.println("Uploaded v2 — latest version");
// Verify v2 is there
byte[] v2 = service.download("alice_token", "config.txt");
System.out.println("Current content: " + new String(v2)); // "Version 2 — important changes"
// Phase 2: Attacker replays v1
System.out.println("Attacker replays v1 upload...");
service.upload("alice_token", "config.txt", "Version 1 content".getBytes());
// Verify — v1 has overwritten v2 silently!
byte[] current = service.download("alice_token", "config.txt");
System.out.println("Content after replay: " + new String(current)); // "Version 1 content"
// Alice's changes are GONE — and she has no idea
The Fix — Nonce + Timestamp
Files: util/NonceStore.java and common/FileOperation.java
The NonceStore
public class NonceStore {
// Track seen nonces with their timestamps for cleanup
private final ConcurrentHashMap<String, Long> seenNonces = new ConcurrentHashMap<>();
private static final long WINDOW_MS = 300_000; // 5 minutes
private final ScheduledExecutorService cleaner;
/**
* Validates a write operation against replay.
*
* Rejects if:
* 1. The timestamp is more than 5 minutes old (stale request)
* 2. The nonce has already been used (replay within window)
*/
public synchronized boolean isValid(String nonce, long timestamp) {
// Check 1: Timestamp freshness
long now = System.currentTimeMillis();
if (Math.abs(now - timestamp) > WINDOW_MS) {
return false; // Request too old
}
// Check 2: Nonce uniqueness
if (seenNonces.containsKey(nonce)) {
return false; // Nonce already used — replay detected
}
seenNonces.put(nonce, timestamp);
return true;
}
// Periodic cleanup: remove nonces older than the window
private void startCleaner() {
cleaner.scheduleAtFixedRate(() -> {
long cutoff = System.currentTimeMillis() - WINDOW_MS;
seenNonces.entrySet().removeIf(e -> e.getValue() < cutoff);
}, 1, 1, TimeUnit.MINUTES);
}
}
The FileOperation Changes
Every write operation now carries two extra fields:
public class FileOperation implements Serializable {
// ... existing fields ...
// Timestamp: when this operation was created (enforces freshness)
private final long timestamp;
// Nonce: UUID generated fresh for every operation (ensures uniqueness)
private final String nonce;
// Both auto-populated by the constructor:
private FileOperation(...) {
this.timestamp = System.currentTimeMillis();
this.nonce = UUID.randomUUID().toString();
}
}
Integration in ReplicaNode
// In handleClientOperation(), BEFORE executing any write:
if (op.isWrite()) {
// FIX 5/BONUS: Validate nonce and timestamp
if (!nonceStore.isValid(op.getNonce(), op.getTimestamp())) {
return new OperationResult(false, "REPLAY_REJECTED: Operation is stale or duplicated");
}
}
Why Both Checks Are Needed
| Attack | Timestamp Check | Nonce Check |
|---|---|---|
| Replay after 10 minutes | ✅ Caught (stale) | ❌ Nonce already cleaned up |
| Replay after 1 second | ❌ Still "fresh" | ✅ Caught (duplicate nonce) |
| Replay after 4 minutes | ❌ Still "fresh" | ✅ Caught (duplicate nonce) |
| Legitimate fresh request | ✅ Passes | ✅ Passes (new nonce) |
Each check catches what the other misses:
- Timestamp catches old replays (outside the 5-minute window)
- Nonce catches recent replays (within the 5-minute window)
Why 5 Minutes?
private static final long WINDOW_MS = 300_000; // 5 minutes
The window exists because the seenNonces set needs to be periodically cleaned — it can't grow forever. The trade-off:
| Window | Pros | Cons |
|---|---|---|
| 1 minute | Small memory footprint | Legitimate operations age out quickly |
| 5 minutes | Good balance | Moderate memory use |
| 1 hour | Catches very old replays | Large memory footprint |
5 minutes is a practical default. The nonce prevents replays within the window; the timestamp rejects anything older.
Testing the Fix
cd secured
java -cp out test.ReplayRejectionTest
Expected output:
[TEST 1] Fresh upload with new nonce... PASSED
[TEST 2] Replay with same nonce... REJECTED (as expected)
[TEST 3] Upload with 10-minute-old timestamp... REJECTED (as expected)
Next: → Mitigations Summary