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.
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.
Network replay attacks occur when attackers intercept network traffic and retransmit it later, using tools like Wireshark or tcpdump. Similarly, 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.
How to Prevent Replay Attacks
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.
A nonce is a randomly generated value included in transmitted data that can prevent replay attacks. Because nonces are generated randomly, attackers cannot accurately guess or reproduce them.
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
Here's an implementation example using Spring Cloud Gateway with Kotlin. This example demonstrates how to prevent replay attacks using authentication tokens, signatures, and nonces stored in Redis with a TTL (Time To Live).
The implementation consists of the following components:
- A gateway filter that validates requests
- A nonce repository that stores and checks for duplicate nonces
- An account repository to retrieve user information and secret keys
- A signature helper to validate request signatures
Let's look at each component:
1. Account Model
package com.giri.springcloudgateway.domain.account
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import java.math.BigInteger
@JsonIgnoreProperties(ignoreUnknown = true)
data class Account(
val id: BigInteger,
val email: String,
val secretKey: String
)
2. Nonce Repository
The NonceRepository uses Redis to store nonces with a TTL and check for duplicates:
package com.giri.springcloudgateway.repository
import org.springframework.data.redis.core.ReactiveStringRedisTemplate
import org.springframework.stereotype.Repository
import reactor.core.publisher.Mono
import java.time.Duration
@Repository
class NonceRepository(private val template: ReactiveStringRedisTemplate) {
companion object {
private const val PREFIX = "NONCE"
}
fun isExists(accessToken: String, nonce: String): Mono<Boolean> {
return template.hasKey("$PREFIX:$accessToken:$nonce")
}
fun setNonce(accessToken: String, nonce: String): Mono<Boolean> {
return template.opsForValue().set("$PREFIX:$accessToken:$nonce", "1", Duration.ofSeconds(10))
}
}
3. Account Repository
The AccountRepository retrieves user account information and implements caching:
package com.giri.springcloudgateway.repository
import com.giri.springcloudgateway.domain.account.Account
import com.giri.springcloudgateway.global.exception.account.AccountServerException
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import org.springframework.core.ParameterizedTypeReference
import org.springframework.stereotype.Repository
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono
import java.time.Duration
@Repository
class AccountRepository(
private val webClient: WebClient
) {
private val cache: Cache<String, List<Account>> = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(10))
.maximumSize(5000)
.build()
fun findAccountByAccessToken(accessToken: String): Mono<List<Account>> {
return cache.getIfPresent(accessToken)?.let { Mono.just(it) }?: webClient.get()
.uri("http://localhost:1010/apis/root-accounts?accessToken=$accessToken")
.retrieve()
.onStatus({it.is4xxClientError || it.is5xxServerError}, { Mono.error(AccountServerException())})
.bodyToMono(object : ParameterizedTypeReference<List<Account>>() {})
.doOnNext { cache.put(accessToken, it) }
}
}
4. Authentication Filter
The AuthenticationFilter is the main component that ties everything together:
package com.giri.springcloudgateway.filter
import com.giri.springcloudgateway.global.exception.*
import com.giri.springcloudgateway.global.helper.SignatureHelper
import com.giri.springcloudgateway.repository.AccountRepository
import com.giri.springcloudgateway.repository.NonceRepository
import org.slf4j.LoggerFactory
import org.springframework.cloud.gateway.filter.GatewayFilter
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory
import org.springframework.http.HttpHeaders
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
@Component
class AuthenticationFilter(
private val accountRepository: AccountRepository,
private val nonceRepository: NonceRepository
): GatewayFilterFactory<AuthenticationFilter.Config> {
private val log = LoggerFactory.getLogger(AuthenticationFilter::class.java)
data class Config(val name: String?)
override fun getConfigClass(): Class<Config> {
return Config::class.java
}
override fun newConfig(): Config {
return Config("AuthenticationFilter")
}
override fun apply(config: Config?): GatewayFilter {
return GatewayFilter { exchange, chain ->
val signature = exchange.request.headers["signature"]?.firstOrNull() ?: throw SignatureNotFoundException()
val nonce = exchange.request.headers["nonce"]?.firstOrNull() ?: throw NonceNotFoundException()
val auth = exchange.request.headers[HttpHeaders.AUTHORIZATION]?.firstOrNull() ?: throw UnauthorizedException()
val accessToken = try {
auth.split(" ")[1]
} catch (e: IndexOutOfBoundsException) {
throw UnauthorizedException()
}
accountRepository.findAccountByAccessToken(accessToken).flatMap {
val account = it.firstOrNull()?: return@flatMap Mono.error(UnauthorizedException())
log.info("{}", account)
if (SignatureHelper.isInvalidSignature(signature, "${exchange.request.method}:${exchange.request.path}:$nonce", account.secretKey)) {
return@flatMap Mono.error(SignatureInvalidException())
}
nonceRepository.isExists(accessToken, nonce).flatMap { exists ->
if (exists) {
Mono.error(DuplicatedNonceException())
} else {
nonceRepository.setNonce(accessToken, nonce)
}
}
}.then(chain.filter(exchange))
}
}
}
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.
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.