Security

How to Prevent Replay Attacks

Updated: April 1, 2024

What is a Replay Attack?

A replay attack is a type of network attack where an attacker intercepts valid network data packets and later reuses them. By retransmitting the data, the system processes it as legitimate data. Replay attacks are difficult to detect because they appear as normal requests. Additionally, they can be successful even if the original transmission was encrypted. Replay attacks can overload systems through repetitive requests, potentially disrupting normal system operations.

Replay Attack Mechanism Sender Receiver Attacker Secure Channel 1. Data capture Modify 2. Replay attack Legend Legitimate entities Attacker 1. Attacker intercepts legitimate data packets 2. Attacker may modify data or use it as-is 3. Receiver processes replayed data as legitimate
As shown in the diagram, the attacker waits until data transmission begins. They then sniff the communication channel to extract the data. The attacker can acquire the data and potentially modify it before reusing it. Although the recipient receives modified data, they treat it as legitimate.
There are four main types of replay attacks: network, wireless, session, and HTTP.

Types of Replay Attacks

  • Network replay attacks occur when attackers intercept network traffic and retransmit it later, using tools like Wireshark or tcpdump.
  • Wireless replay attacks involve intercepting wireless communications and then retransmitting them.
  • Session replay attacks intercept sessions between two parties.
  • HTTP replay attacks involve capturing HTTP requests and responses to execute the attack.

Real-World Example

Let's assume Alice wants to log into her online banking account using a web browser. When Alice enters her login credentials and clicks the submit button, the login request is transmitted to the bank server over the internet.

Bob, the attacker, monitors the network and captures the login request as it's being transmitted. He then waits until Alice logs out of her account before resending the captured login request to the bank server. Since the login request is valid, the server accepts it and grants Bob access to Alice's account.

Banking Replay Attack Example Alice Login Bank Server Bob (Attacker) Internet 1 Login request 2 Capture 3 Server processes request 4 Alice logs out 5 Replay login request 6 Access granted Access to Alice's account Legend Legitimate user Attacker Bank server

How to Prevent Replay Attacks

There are several methods to prevent replay attacks. The most common approaches include:

Prevention Methods

  • Timestamp-based validation: Include a timestamp in each request and reject requests that are too old.
  • Nonce-based validation: Use a unique number (nonce) for each request and reject duplicate nonces.
  • Sequence numbers: Use incrementing sequence numbers to ensure requests are processed in order.
  • Challenge-response authentication: Require the client to prove knowledge of a secret without transmitting it.

Message Authentication Codes with Timestamps or Nonces

One approach is to use Message Authentication Codes (MACs). A MAC is a cryptographic checksum included in transmitted data that ensures authenticity and integrity. The MAC can incorporate a timestamp or other changing value for each transmission, making it difficult for attackers to reuse captured transmissions.

Including timestamps in transmitted data helps prevent replay attacks by ensuring the data is only considered valid within a specific timeframe. Nonces (Number used Only Once) can also be used during data transmission across networks.

JWT ID (JTI)

When using JWTs (JSON Web Tokens) created by clients, you can include a JTI in the payload. JTI stands for JWT ID and can uniquely identify a JWT. This allows clients to use JWTs as MACs before discarding them. The server can store the JWT's JTI in a cache or database and reject requests with the same JTI, considering them replay attacks.

Example Implementation

Let's implement a solution to prevent replay attacks using Spring Cloud Gateway with Kotlin. This implementation will include:

Implementation Components

  • Gateway Filter: Validates requests and prevents replay attacks
  • Nonce Repository: Stores and manages nonces
  • Account Repository: Manages account information
  • Signature Helper: Handles request signing and verification

Account Model

data class Account(
    val id: String,
    val apiKey: String,
    val secretKey: String,
    val status: AccountStatus
)

enum class AccountStatus {
    ACTIVE,
    INACTIVE,
    SUSPENDED
}

Nonce Repository

interface NonceRepository {
    fun saveNonce(nonce: String, ttlSeconds: Long): Boolean
    fun existsNonce(nonce: String): Boolean
}

@Component
class RedisNonceRepository(
    private val redisTemplate: StringRedisTemplate
) : NonceRepository {
    override fun saveNonce(nonce: String, ttlSeconds: Long): Boolean {
        return redisTemplate.opsForValue()
            .setIfAbsent("nonce:$nonce", "1", Duration.ofSeconds(ttlSeconds))
    }

    override fun existsNonce(nonce: String): Boolean {
        return redisTemplate.hasKey("nonce:$nonce")
    }
}

Account Repository

interface AccountRepository {
    fun findByApiKey(apiKey: String): Account?
}

@Component
class JpaAccountRepository(
    private val accountJpaRepository: AccountJpaRepository
) : AccountRepository {
    override fun findByApiKey(apiKey: String): Account? {
        return accountJpaRepository.findByApiKey(apiKey)?.toDomain()
    }
}

Authentication Filter

@Component
class AuthenticationFilter(
    private val accountRepository: AccountRepository,
    private val nonceRepository: NonceRepository,
    private val signatureHelper: SignatureHelper
) : GlobalFilter {
    override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
        val request = exchange.request
        val response = exchange.response

        // Skip authentication for certain paths
        if (shouldSkipAuth(request)) {
            return chain.filter(exchange)
        }

        return try {
            val apiKey = request.headers.getFirst("X-API-Key")
                ?: throw UnauthorizedException("API key is required")

            val account = accountRepository.findByApiKey(apiKey)
                ?: throw UnauthorizedException("Invalid API key")

            if (account.status != AccountStatus.ACTIVE) {
                throw UnauthorizedException("Account is not active")
            }

            val nonce = request.headers.getFirst("X-Nonce")
                ?: throw UnauthorizedException("Nonce is required")

            if (nonceRepository.existsNonce(nonce)) {
                throw UnauthorizedException("Nonce already used")
            }

            val signature = request.headers.getFirst("X-Signature")
                ?: throw UnauthorizedException("Signature is required")

            val requestPath = request.path.toString()
            val requestMethod = request.method.toString()
            val requestBody = request.body.toString()

            val expectedSignature = signatureHelper.generateSignature(
                account.secretKey,
                requestPath,
                requestMethod,
                requestBody,
                nonce
            )

            if (signature != expectedSignature) {
                throw UnauthorizedException("Invalid signature")
            }

            // Save nonce after successful validation
            nonceRepository.saveNonce(nonce, 300) // 5 minutes TTL

            chain.filter(exchange)
        } catch (e: UnauthorizedException) {
            response.statusCode = HttpStatus.UNAUTHORIZED
            response.headers.contentType = MediaType.APPLICATION_JSON
            val responseBody = mapOf(
                "error" to "Unauthorized",
                "message" to e.message
            )
            val buffer = response.bufferFactory().wrap(
                ObjectMapper().writeValueAsString(responseBody).toByteArray()
            )
            response.writeWith(Mono.just(buffer))
        }
    }

    private fun shouldSkipAuth(request: ServerHttpRequest): Boolean {
        val path = request.path.toString()
        return path.startsWith("/public/") || path.startsWith("/health")
    }
}

Implementation Notes

  • The TTL period (10 seconds in this example) should be adjusted based on your system's needs. Longer TTLs increase security against replay attacks but require more storage.
  • Using Redis for nonce storage enables horizontal scaling of your API gateway.
  • The signature should include the HTTP method, path, and nonce to ensure that the signature is unique for each request, even if the same endpoint is called.

How This Prevents Replay Attacks

This implementation prevents replay attacks through several mechanisms:

  • Signature Verification: Each request must include a valid signature created using the HTTP method, request path, nonce, and a secret key known only to the client and server.
  • Nonce Validation: Each request must include a unique nonce that hasn't been used before within the TTL period.
  • TTL-based Storage: Nonces are stored in Redis with a time-to-live (TTL) of 10 seconds, after which they expire automatically.
  • Access Token Binding: Nonces are tied to specific access tokens, ensuring that even if a nonce is reused by a different user, it will be rejected.

When a request is received, the filter validates the signature and checks if the nonce has been used before. If the nonce is new, it's stored in Redis with a TTL. If a replay attack attempts to reuse the same nonce within the TTL period, the filter will detect the duplicate nonce and reject the request.

References

Advertisement