Files
tamabox/internal/security/honeypot/honeypot_test.go
T
tamakyi 7d52155411 fix(honeypot): 蜜罐封禁与 enable_auto_ban 解耦 + 泄露扫描后缀匹配
- autoban: 新增 RecordHoneypot 独立通道(不受 enabled 约束)+ 独立阈值/时长;
  IsBanned 不再因 enable_auto_ban=false 而忽略已有封禁
- honeypot: 新增 honeypotSuffixes 后缀匹配,覆盖 /backup.zip、/db.sql、/id_rsa.pem
- 新增配置 honeypot_threshold / honeypot_ban_minutes
- 补测试:autoban/honeypot_test.go、route 侧禁用自动封禁仍封禁
2026-10-21 12:36:33 +08:00

151 lines
4.7 KiB
Go

package honeypot
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestMatch(t *testing.T) {
h := New(Options{Enabled: true})
hit := []string{
"/wp-admin", "/wp-admin/install.php", "/wp-login.php", "/wp-content/plugins/",
"/.ssh/id_rsa", "/.env", "/.git/config", "/.aws/credentials",
"/administrator", "/phpmyadmin", "/actuator/env", "/swagger-ui.html",
"/xmlrpc.php", "/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php",
"/wp-admin/../wp-content/x.php",
// 后缀匹配:备份/配置/密钥泄露扫描
"/backup.zip", "/www.zip", "/site.tar.gz", "/db.sql", "/dump.sql",
"/x.bak", "/config.php.bak", "/.env.bak", "/id_rsa.pem", "/private.key",
"/server.jks", "/data.ovpn",
}
for _, p := range hit {
if !h.Match(p) {
t.Errorf("Match(%q) = false, want true", p)
}
}
// 业务路由必须一律不接管 —— 这是蜜罐的红线。
miss := []string{
"/", "/login", "/register", "/logout", "/_/alice", "/user", "/user/settings",
"/api/v1/questions", "/static/css/base.css", "/favicon.ico", "/robots.txt",
"/about", "/rules", "/sponsor", "/change-logs.html", "/vote/abc",
"/explore", "/i18n/en",
// 业务 URL 空间里不存在这些后缀
"/_/admin.ini", "/api/v1/questions.json", "/user/settings.yaml",
}
for _, p := range miss {
if h.Match(p) {
t.Errorf("Match(%q) = true, want false(业务路由不得被蜜罐接管)", p)
}
}
}
func TestMatchDisabled(t *testing.T) {
h := New(Options{Enabled: false})
if h.Match("/wp-admin") {
t.Error("蜜罐未启用时不应匹配任何路径")
}
}
func TestMatchEncoded(t *testing.T) {
h := New(Options{Enabled: true})
// %2f 编码绕过
if !h.Match("/wp-admin%2f..%2fetc") {
t.Error("编码后的 wp-admin 路径应被匹配")
}
// 大小写
if !h.Match("/WP-ADMIN") {
t.Error("大小写不同的 WP-ADMIN 应被匹配")
}
// 查询串不应影响判定
if !h.Match("/wp-login.php?redirect_to=/wp-admin") {
t.Error("带查询串的 wp-login.php 应被匹配")
}
}
func TestExtraPaths(t *testing.T) {
h := New(Options{Enabled: true, ExtraPaths: []string{"/secret-backdoor"}})
if !h.Match("/secret-backdoor") {
t.Error("自定义诱饵路径应被匹配")
}
if !h.Match("/secret-backdoor/x.php") {
t.Error("自定义诱饵路径的子路径应被匹配")
}
}
func TestExtractCredentialsForm(t *testing.T) {
h := New(Options{Enabled: true})
req := httptest.NewRequest(http.MethodPost, "/wp-login.php",
strings.NewReader("log=admin&pwd=admin123&redirect_to=%2Fwp-admin%2F&wp-submit=Log+In"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
creds := h.ExtractCredentials(req)
if creds.Username != "admin" {
t.Errorf("Username = %q, want admin", creds.Username)
}
if creds.Password != "admin123" {
t.Errorf("Password = %q, want admin123", creds.Password)
}
if !creds.HadFields {
t.Error("HadFields = false, want true")
}
}
func TestExtractCredentialsJSON(t *testing.T) {
h := New(Options{Enabled: true})
req := httptest.NewRequest(http.MethodPost, "/wp-login.php",
strings.NewReader(`{"username":"root","password":"toor","extra":1}`))
req.Header.Set("Content-Type", "application/json")
creds := h.ExtractCredentials(req)
if creds.Username != "root" {
t.Errorf("Username = %q, want root", creds.Username)
}
if creds.Password != "toor" {
t.Errorf("Password = %q, want toor", creds.Password)
}
}
func TestExtractCredentialsQuery(t *testing.T) {
h := New(Options{Enabled: true})
req := httptest.NewRequest(http.MethodGet, "/wp-login.php?log=admin&pwd=secret", nil)
creds := h.ExtractCredentials(req)
if creds.Username != "admin" || creds.Password != "secret" {
t.Errorf("got %q/%q, want admin/secret", creds.Username, creds.Password)
}
}
func TestExtractCredentialsBasicAuth(t *testing.T) {
h := New(Options{Enabled: true})
req := httptest.NewRequest(http.MethodGet, "/wp-admin", nil)
req.SetBasicAuth("admin", "hunter2")
creds := h.ExtractCredentials(req)
if creds.Username != "admin" || creds.Password != "hunter2" {
t.Errorf("got %q/%q, want admin/hunter2", creds.Username, creds.Password)
}
}
func TestExtractCredentialsEmpty(t *testing.T) {
h := New(Options{Enabled: true})
req := httptest.NewRequest(http.MethodGet, "/wp-admin", nil)
creds := h.ExtractCredentials(req)
if creds.HadFields {
t.Error("空请求不应标记为携带凭据")
}
}
func TestExtractCredentialsMalformed(t *testing.T) {
h := New(Options{Enabled: true})
// 畸形 body 不应 panic
req := httptest.NewRequest(http.MethodPost, "/wp-login.php",
strings.NewReader("%zz&log=admin&&=="))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
creds := h.ExtractCredentials(req)
if creds.Username != "admin" {
t.Errorf("Username = %q, want admin(畸形 body 仍应尽量提取)", creds.Username)
}
}