Skip to content

Commit 40a326a

Browse files
authored
security: validate key query param and fix /count return type (#231)
- server: validate the key query parameter against a strict charset (letters, digits, _ : -, max 64) so the API cannot be used to probe arbitrary redis keys; invalid keys return 400 - server: /count without a key returned a bare int which Flask rejects; always return a string - README: document API_KEY and add a security note about exposing the pool publicly without authentication Refs #228
1 parent c355a41 commit 40a326a

2 files changed

Lines changed: 25 additions & 5 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ get random proxy 116.196.115.209:8080
259259
- API_HOST:代理 Server 运行 Host,默认 0.0.0.0
260260
- API_PORT:代理 Server 运行端口,默认 5555
261261
- API_THREADED:代理 Server 是否使用多线程,默认 true
262+
- API_KEY:API 访问鉴权密钥,默认空(即不鉴权)。设置后,调用 `/random``/all``/count` 需在请求头携带 `API-KEY`,详见下方「安全性」说明
263+
264+
> ⚠️ 安全提示:代理 Server 默认监听 `0.0.0.0``API_KEY` 默认为空,任何能访问该端口的人都可以调用 `/random``/all``/count`。如果将代理池**暴露到公网**,请务必设置 `API_KEY`,并配合防火墙/安全组限制来源。`key` 查询参数已做格式校验,仅允许字母、数字及 `_ : -`,最长 64 位。
262265
263266
### 日志
264267

proxypool/processors/server.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import hmac
2-
from flask import Flask, g, request
2+
import re
3+
from flask import Flask, g, request, abort
34
from proxypool.exceptions import PoolEmptyException
45
from proxypool.storages.redis import RedisClient
56
from proxypool.setting import API_HOST, API_PORT, API_THREADED, API_KEY, IS_DEV, PROXY_RAND_KEY_DEGRADED
@@ -11,6 +12,10 @@
1112
if IS_DEV:
1213
app.debug = True
1314

15+
# allowed characters for the `key` query parameter that selects a redis sub-pool;
16+
# restricts to a safe charset to avoid probing arbitrary redis keys via the API
17+
VALID_KEY_PATTERN = re.compile(r'^[a-zA-Z0-9_:\-]{1,64}$')
18+
1419

1520
def auth_required(func):
1621
@functools.wraps(func)
@@ -41,6 +46,18 @@ def get_conn():
4146
return g.redis
4247

4348

49+
def get_request_key():
50+
"""
51+
read the `key` query parameter and validate its format;
52+
reject unexpected characters to avoid redis key probing/injection
53+
:return: validated key or None
54+
"""
55+
key = request.args.get('key')
56+
if key and not VALID_KEY_PATTERN.match(key):
57+
abort(400, description='invalid key parameter')
58+
return key
59+
60+
4461
@app.route('/')
4562
@auth_required
4663
def index():
@@ -59,7 +76,7 @@ def get_proxy():
5976
if PROXY_RAND_KEY_DEGRADED is set to True, will get a universal random proxy if no proxy found in the sub-pool
6077
:return: get a random proxy
6178
"""
62-
key = request.args.get('key')
79+
key = get_request_key()
6380
conn = get_conn()
6481
# return conn.random(key).string() if key else conn.random().string()
6582
if key:
@@ -78,7 +95,7 @@ def get_proxy_all():
7895
get a random proxy
7996
:return: get a random proxy
8097
"""
81-
key = request.args.get('key')
98+
key = get_request_key()
8299

83100
conn = get_conn()
84101
proxies = conn.all(key) if key else conn.all()
@@ -98,8 +115,8 @@ def get_count():
98115
:return: count, int
99116
"""
100117
conn = get_conn()
101-
key = request.args.get('key')
102-
return str(conn.count(key)) if key else conn.count()
118+
key = get_request_key()
119+
return str(conn.count(key)) if key else str(conn.count())
103120

104121

105122
if __name__ == '__main__':

0 commit comments

Comments
 (0)