Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add replay protection plugin #1672

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d46723d
"feat: add replay protection plugin
yunmaoQu Jan 14, 2025
c0cff2b
Merge branch 'main' into feature/request-replay-protection
yunmaoQu Jan 14, 2025
d894e3d
update
yunmaoQu Jan 15, 2025
9a9e2e4
Merge branch 'alibaba:main' into feature/request-replay-protection
yunmaoQu Jan 15, 2025
9c0f1dc
Merge remote-tracking branch 'origin' into feature/request-replay-pro…
yunmaoQu Jan 15, 2025
506208f
Merge branch 'feature/request-replay-protection' of https://github.co…
yunmaoQu Jan 15, 2025
d7cec7f
update
yunmaoQu Jan 16, 2025
2762111
update
yunmaoQu Jan 16, 2025
ce47411
Merge branch 'main' into feature/request-replay-protection
yunmaoQu Jan 17, 2025
935d1c9
update
yunmaoQu Jan 20, 2025
a6a5af4
Merge branch 'feature/request-replay-protection' of https://github.co…
yunmaoQu Jan 20, 2025
b1e4ddb
Merge branch 'main' into feature/request-replay-protection
yunmaoQu Jan 20, 2025
1cfd691
update
yunmaoQu Jan 21, 2025
8dccbab
Merge branch 'feature/request-replay-protection' of https://github.co…
yunmaoQu Jan 21, 2025
7caad55
Merge branch 'main' into feature/request-replay-protection
yunmaoQu Jan 21, 2025
b694c48
update
yunmaoQu Jan 21, 2025
ed892cf
Merge branch 'feature/request-replay-protection' of https://github.co…
yunmaoQu Jan 21, 2025
50c088d
fix
yunmaoQu Jan 24, 2025
ed33204
fix
yunmaoQu Jan 24, 2025
88504b0
fix
yunmaoQu Jan 30, 2025
6ee8499
fix
yunmaoQu Jan 31, 2025
5707224
fix e2e test
hanxiantao Feb 9, 2025
d1775a1
fix e2e test
hanxiantao Feb 9, 2025
8792448
add TestCaseName
hanxiantao Feb 9, 2025
67d50ea
add TestCaseName
hanxiantao Feb 9, 2025
ab6a397
fix e2e test
yunmaoQu Feb 9, 2025
f27e70d
Merge branch 'main' into feature/request-replay-protection
hanxiantao Feb 22, 2025
64b8564
请求防重放插件问题修复
hanxiantao Feb 22, 2025
fc18055
fix e2e test
hanxiantao Feb 22, 2025
c6ca553
fix e2e test
hanxiantao Feb 23, 2025
29dd572
Update README.md
hanxiantao Feb 23, 2025
4f82312
Merge branch 'main' into feature/request-replay-protection
hanxiantao Feb 26, 2025
ec6aeb6
Add database configuration for plugins that use Redis.
hanxiantao Feb 26, 2025
0b358dc
Update README.md
hanxiantao Feb 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions plugins/wasm-go/extensions/replay-protection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
title: 防重放攻击
keywords: [higress,replay-protection]
description: 防重放攻击插件配置参考
---

## 功能说明

防重放插件通过验证请求中的一次性随机数来防止请求重放攻击。每个请求都需要携带一个唯一的 nonce 值,服务器会记录并校验这个值的唯一性,从而防止请求被恶意重放

具体包含一下功能:

- **强制或可选的 nonce 校验**:可根据配置决定是否强制要求请求携带 nonce 值。
- **基于 Redis 的 nonce 唯一性验证**:通过 Redis 存储和校验 nonce 值,确保其唯一性。
- **可配置的 nonce 有效期**:支持设置 nonce 的有效期,过期后自动失效。
- **nonce 格式和长度校验**:支持对 nonce 值的格式(Base64)和长度进行验证。
- **自定义错误响应**:支持配置拒绝请求时的状态码和错误信息。
- **可自定义 nonce 请求头**:可以自定义携带 nonce 的请求头名称。

## 运行属性

插件执行阶段:`认证阶段`
插件执行优先级:`800`

## 配置字段

| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|----------------------|--------|------|-----------------|---------------------------------|
| `force_nonce` | bool | 否 | true | 是否强制要求请求携带 nonce 值 |
| `nonce_header` | string | 否 | `X-Higress-Nonce` | 指定携带 nonce 值的请求头名称 |
| `nonce_ttl` | int | 否 | 900 | nonce 的有效期,单位秒 |
| `nonce_min_length` | int | 否 | 8 | nonce 值的最小长度 |
| `nonce_max_length` | int | 否 | 128 | nonce 值的最大长度 |
| `reject_code` | int | 否 | 429 | 拒绝请求时返回的状态码 |
| `reject_msg` | string | 否 | `Replay Attack Detected` | 拒绝请求时返回的错误信息 |
| `validate_base64` | bool | 否 | false | 是否校验 nonce 的 base64 编码格式 |
| `redis` | Object | 是 | - | redis 相关配置 |

`redis` 中每一项的配置字段说明

| 名称 | 数据类型 | 必填 | 默认值 | 描述|
| -------------- | -------- | ---- |---------------------| --------------------------------------- |
| `service_name` | string | 是 | - | redis 服务名称,带服务类型的完整 FQDN 名称,例如 my-redis.dns、redis.my-ns.svc.cluster.local |
| `service_port` | int | 否 | 6379 | redis 服务端口|
| `username` | string | 否 | - | redis 用户名|
| `password` | string | 否 | - | redis 密码|
| `timeout` | int | 否 | 1000 | redis 连接超时时间,单位毫秒 |
| database | int | 否 | 0 | 使用的数据库id,例如配置为1,对应`SELECT 1`|
| `key_prefix` | string | 否 | `replay-protection` | redis 键前缀,用于区分不同的 nonce 键 |

## 配置示例

以下是一个防重放攻击插件的完整配置示例:

```yaml
force_nonce: true
nonce_header: "X-Higress-Nonce" # 指定 nonce 请求头名称
nonce_ttl: 900 # nonce 有效期,设置为 900 秒
nonce_min_length: 8 # nonce 的最小长度
nonce_max_length: 128 # nonce 的最大长度
validate_base64: true # 是否开启 base64 格式校验
reject_code: 429 # 当拒绝请求时返回的 HTTP 状态码
reject_msg: "Replay Attack Detected" # 拒绝请求时返回的错误信息内容
redis:
service_name: redis.static # Redis 服务的名称
service_port: 80 # Redis 服务所使用的端口
timeout: 1000 # Redis 操作的超时时间(单位:毫秒)
key_prefix: "replay-protection" # Redis 中键的前缀
```

## 使用说明

### 请求头要求

| 请求头名称 | 是否必须 | 说明 |
|-----------------|----------------|------------------------------------------|
| `X-Higress-Nonce` | 根据 `force_nonce` 配置决定 | 请求中携带的随机生成的 nonce 值,需符合 Base64 格式。 |

> **注意**:可以通过 `nonce_header` 配置自定义请求头名称,默认值为 `X-Higress-Nonce`。

### 使用示例

```bash
# Generate nonce
nonce=$(openssl rand -base64 32)

# Send request
curl -X POST 'https://api.example.com/path' \
-H "X-Higress-Nonce: $nonce" \
-d '{"key": "value"}'
```

## 返回结果

```json
{
"code": 429,
"message": "Replay Attack Detected"
}
```

## 错误响应示例

| 错误场景 | 状态码 | 错误信息 |
|------------------------|-------|--------------------|
| 缺少 nonce 请求头 | 400 | `Missing Required Header` |
| nonce 长度不符合要求 | 400 | `Invalid Nonce` |
| nonce 格式不符合 Base64 | 400 | `Invalid Nonce` |
| nonce 已被使用(重放攻击) | 429 | `Replay Attack Detected` |

109 changes: 109 additions & 0 deletions plugins/wasm-go/extensions/replay-protection/README_EN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
title: Replay Attack Prevention
keywords: [higress, replay-protection]
description: Configuration reference for the replay attack prevention plugin
---

## Functional Description

The replay prevention plugin prevents request replay attacks by verifying the one-time random number in the request. Each request needs to carry a unique nonce value. The server will record and verify the uniqueness of this value, thus preventing requests from being maliciously replayed.

Specifically, it includes the following functions:

- **Mandatory or Optional Nonce Verification**: It can be configured to determine whether requests are required to carry a nonce value.
- **Nonce Uniqueness Verification Based on Redis**: The nonce value is stored and verified in Redis to ensure its uniqueness.
- **Configurable Nonce Validity Period**: It supports setting the validity period of the nonce, which will automatically expire after the period.
- **Nonce Format and Length Verification**: It supports verifying the format (Base64) and length of the nonce value.
- **Custom Error Response**: It supports configuring the status code and error message when a request is rejected.
- **Customizable Nonce Request Header**: The name of the request header carrying the nonce can be customized.

## Runtime Attributes

Plugin execution stage: `Authentication Stage`
Plugin execution priority: `800`

## Configuration Fields

| Name | Data Type | Required | Default Value | Description |
|----------------------|--------|------|-----------------|---------------------------------|
| `force_nonce` | bool | No | true | Whether requests are required to carry a nonce value. |
| `nonce_header` | string | No | `X-Higress-Nonce` | Specifies the name of the request header carrying the nonce value. |
| `nonce_ttl` | int | No | 900 | The validity period of the nonce, in seconds. |
| `nonce_min_length` | int | No | 8 | The minimum length of the nonce value. |
| `nonce_max_length` | int | No | 128 | The maximum length of the nonce value. |
| `reject_code` | int | No | 429 | The status code returned when a request is rejected. |
| `reject_msg` | string | No | `Replay Attack Detected` | The error message returned when a request is rejected. |
| `validate_base64` | bool | No | false | Whether to verify the Base64 encoding format of the nonce. |
| `redis` | Object | Yes | - | Redis-related configuration |

Description of each configuration field in `redis`

| Name | Data Type | Required | Default Value | Description|
| -------------- | -------- | ---- |---------------------| --------------------------------------- |
| `service_name` | string | Yes | - | The name of the Redis service, the complete FQDN name with the service type, such as my-redis.dns, redis.my-ns.svc.cluster.local. |
| `service_port` | int | No | 6379 | The port of the Redis service. |
| `username` | string | No | - | The username of Redis. |
| `password` | string | No | - | The password of Redis. |
| `timeout` | int | No | 1000 | The connection timeout time of Redis, in milliseconds. |
| `database` | int | No | 0 | The ID of the database to be used. For example, if it is configured as 1, it corresponds to `SELECT 1`. |
| `key_prefix` | string | No | `replay-protection` | The key prefix of Redis, used to distinguish different nonce keys. |

## Configuration Example

The following is a complete configuration example of the replay attack prevention plugin:

```yaml
force_nonce: true
nonce_header: "X-Higress-Nonce" # Specifies the name of the nonce request header
nonce_ttl: 900 # The validity period of the nonce, set to 900 seconds
nonce_min_length: 8 # The minimum length of the nonce
nonce_max_length: 128 # The maximum length of the nonce
validate_base64: true # Whether to enable Base64 format verification
reject_code: 429 # The HTTP status code returned when a request is rejected
reject_msg: "Replay Attack Detected" # The error message content returned when a request is rejected
redis:
service_name: redis.static # The name of the Redis service
service_port: 80 # The port used by the Redis service
timeout: 1000 # The timeout time of Redis operations (unit: milliseconds)
key_prefix: "replay-protection" # The key prefix in Redis
```

## Usage Instructions

### Request Header Requirements

| Request Header Name | Required | Description |
|-----------------|----------------|------------------------------------------|
| `X-Higress-Nonce` | Determined by the `force_nonce` configuration | The randomly generated nonce value carried in the request, which needs to conform to the Base64 format. |

> **Note**: The name of the request header can be customized through the `nonce_header` configuration. The default value is `X-Higress-Nonce`.

### Usage Example

```bash
# Generate nonce
nonce=$(openssl rand -base64 32)

# Send request
curl -X POST 'https://api.example.com/path' \
-H "X-Higress-Nonce: $nonce" \
-d '{"key": "value"}'
```

## Return Results

```json
{
"code": 429,
"message": "Replay Attack Detected"
}
```

## Error Response Examples

| Error Scenario | Status Code | Error Message |
|------------------------|-------|--------------------|
| Missing nonce request header | 400 | `Missing Required Header` |
| Nonce length does not meet the requirements | 400 | `Invalid Nonce` |
| Nonce format does not conform to Base64 | 400 | `Invalid Nonce` |
| Nonce has been used (replay attack) | 429 | `Replay Attack Detected` |
1 change: 1 addition & 0 deletions plugins/wasm-go/extensions/replay-protection/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.0.0-alpha
107 changes: 107 additions & 0 deletions plugins/wasm-go/extensions/replay-protection/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package config

import (
"fmt"
"strings"

"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/tidwall/gjson"
)

type ReplayProtectionConfig struct {
ForceNonce bool // Whether to enforce nonce verification
NonceTTL int // Expiration time of the nonce (in seconds)
Redis RedisConfig
NonceMinLen int // Minimum length of the nonce
NonceMaxLen int // Maximum length of the nonce
NonceHeader string // Name of the nonce header
ValidateBase64 bool // Whether to validate base64 encoding format
RejectCode uint32 // Response code
RejectMsg string // Response body
}

type RedisConfig struct {
Client wrapper.RedisClient
KeyPrefix string
}

func ParseConfig(json gjson.Result, config *ReplayProtectionConfig, log wrapper.Log) error {
// Parse Redis configuration
redisConfig := json.Get("redis")
if !redisConfig.Exists() {
return fmt.Errorf("missing redis config")
}

serviceName := redisConfig.Get("service_name").String()
if serviceName == "" {
return fmt.Errorf("redis service name is required")
}

servicePort := redisConfig.Get("service_port").Int()
if servicePort == 0 {
if strings.HasSuffix(serviceName, ".static") {
servicePort = 80 // default logic port for static service
} else {
servicePort = 6379
}
}

username := redisConfig.Get("username").String()
password := redisConfig.Get("password").String()
timeout := redisConfig.Get("timeout").Int()
if timeout == 0 {
timeout = 1000
}

// Initialize Redis client
config.Redis.Client = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
Port: servicePort,
})
database := int(redisConfig.Get("database").Int())
if err := config.Redis.Client.Init(username, password, timeout, wrapper.WithDataBase(database)); err != nil {
return err
}

keyPrefix := redisConfig.Get("key_prefix").String()
if keyPrefix == "" {
keyPrefix = "replay-protection"
}
config.Redis.KeyPrefix = keyPrefix

config.NonceHeader = json.Get("nonce_header").String()
if config.NonceHeader == "" {
config.NonceHeader = "X-Higress-Nonce"
}

config.ValidateBase64 = json.Get("validate_base64").Bool()

config.RejectCode = uint32(json.Get("reject_code").Int())
if config.RejectCode == 0 {
config.RejectCode = 429
}

config.RejectMsg = json.Get("reject_msg").String()
if config.RejectMsg == "" {
config.RejectMsg = "Replay Attack Detected"
}

config.ForceNonce = json.Get("force_nonce").Bool()

config.NonceTTL = int(json.Get("nonce_ttl").Int())
if config.NonceTTL == 0 {
config.NonceTTL = 900
}

config.NonceMinLen = int(json.Get("nonce_min_length").Int())
if config.NonceMinLen == 0 {
config.NonceMinLen = 8
}

config.NonceMaxLen = int(json.Get("nonce_max_length").Int())
if config.NonceMaxLen == 0 {
config.NonceMaxLen = 128
}

return nil
}
22 changes: 22 additions & 0 deletions plugins/wasm-go/extensions/replay-protection/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module replay-protection

go 1.19

replace github.com/alibaba/higress/plugins/wasm-go => ../..

require (
github.com/alibaba/higress/plugins/wasm-go v1.4.2
github.com/higress-group/proxy-wasm-go-sdk v1.0.0
github.com/tidwall/gjson v1.18.0
github.com/tidwall/resp v0.1.1
)

require (
github.com/google/uuid v1.3.0 // indirect
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
github.com/magefile/mage v1.14.0 // indirect
github.com/tetratelabs/wazero v1.7.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/wasilibs/go-re2 v1.5.3 // indirect
)
24 changes: 24 additions & 0 deletions plugins/wasm-go/extensions/replay-protection/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKEak5MWjBDhWjuHijU=
github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsefv8GU57h9CCLYGJA74/tF5/0=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/tetratelabs/wazero v1.7.1 h1:QtSfd6KLc41DIMpDYlJdoMc6k7QTN246DM2+n2Y/Dx8=
github.com/tetratelabs/wazero v1.7.1/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
github.com/wasilibs/go-re2 v1.5.3 h1:wiuTcgDZdLhu8NG8oqF5sF5Q3yIU14lPAvXqeYzDK3g=
github.com/wasilibs/go-re2 v1.5.3/go.mod h1:PzpVPsBdFC7vM8QJbbEnOeTmwA0DGE783d/Gex8eCV8=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Loading
Loading