Skip to content

Commit 0db2dba

Browse files
committed
fix: 收紧响应头身份字段识别
Server / X-Powered-By 字段仅信任第一段产品名(避免「openresty, Microsoft-IIS/10.0」这种伪造糊弄)、命中后即停;当响应头中同时出现 4 个及以上主体身份字段(server、x-powered-by、x-aspnet-version、x-drupal-cache、x-powered-cms、x-varnish 等)或 server 自身就拼了多个产品,则把 Web 服务器、网站程序、后端框架、运行时类目的识别降级到低置信度并附伪造警示。 将版本号提升到 1.3.33。
1 parent 7dd7cb6 commit 0db2dba

2 files changed

Lines changed: 56 additions & 2 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "stackprism",
33
"private": true,
4-
"version": "1.3.32",
4+
"version": "1.3.33",
55
"type": "module",
66
"description": "StackPrism 用于检测网页前端、后端、CDN、SaaS、广告营销、统计、登录、支付、网站程序和主题模板线索。",
77
"scripts": {

src/background/headers.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,62 @@ const applyHeaderRuleList = (
8181
const applyHeaderValueRuleList = (add: any, rules: any[], value: string, rawValue: string, headerName: string) => {
8282
if (!value || !Array.isArray(rules) || !rules.length) return
8383

84+
// Server / X-Powered-By 字段正常只对应一个产品;带逗号往往是反代叠加或伪造
85+
// 只匹配第一段,避免被「openresty, Microsoft-IIS/10.0」这种伪造糊弄
86+
const primaryValue = headerName === 'server' || headerName === 'x-powered-by' ? value.split(',')[0].trim() : value
87+
if (!primaryValue) return
88+
8489
for (const rule of rules) {
85-
if (!matchesHeaderPatterns(rule.patterns, value, rule)) continue
90+
if (!matchesHeaderPatterns(rule.patterns, primaryValue, rule)) continue
8691
const evidence = rule.evidence || `${headerName}: ${rawValue}`
8792
add(rule.category || '其他库', rule.name, rule.confidence || '高', evidence)
93+
if (headerName === 'server' || headerName === 'x-powered-by') break // 这两个字段正常只标识一种产品
94+
}
95+
}
96+
97+
// 主体身份响应头:每一项理论上只对应一种栈,集齐很多个不同身份就是被伪造的强信号
98+
const SPOOF_INDICATOR_HEADERS = [
99+
'server',
100+
'x-powered-by',
101+
'x-aspnet-version',
102+
'x-aspnetmvc-version',
103+
'x-drupal-cache',
104+
'x-drupal-dynamic-cache',
105+
'x-generator',
106+
'x-powered-cms',
107+
'x-varnish',
108+
'x-rails-version',
109+
'x-runtime',
110+
'x-php-version',
111+
'x-jenkins',
112+
'x-cocoon-version'
113+
]
114+
115+
const SPOOF_PRONE_CATEGORIES = new Set(['Web 服务器', '网站程序', '后端 / 服务器框架', '开发语言 / 运行时', 'CMS / 电商平台'])
116+
117+
const countSpoofIndicators = (headers: Record<string, string>): number => {
118+
let count = 0
119+
for (const name of SPOOF_INDICATOR_HEADERS) {
120+
const value = headers[name]
121+
if (typeof value === 'string' && value.trim()) count += 1
122+
}
123+
return count
124+
}
125+
126+
const SPOOF_NOTICE = '响应头里同时出现多种不同主体身份字段,识别结果可能被伪造'
127+
128+
const markSpoofedHeaderDetections = (technologies: any[], headers: Record<string, string>): void => {
129+
const indicatorCount = countSpoofIndicators(headers)
130+
// server 自身就带多个产品 / 出现 4+ 个不同身份字段:视为伪造
131+
const serverHasMultiple = typeof headers.server === 'string' && headers.server.includes(',')
132+
if (indicatorCount < 4 && !serverHasMultiple) return
133+
for (const tech of technologies) {
134+
if (!SPOOF_PRONE_CATEGORIES.has(tech.category)) continue
135+
tech.confidence = '低'
136+
const evidence: string[] = Array.isArray(tech.evidence) ? tech.evidence : tech.evidence ? [tech.evidence] : []
137+
if (!evidence.some((line: string) => typeof line === 'string' && line.includes(SPOOF_NOTICE))) {
138+
tech.evidence = [SPOOF_NOTICE, ...evidence]
139+
}
88140
}
89141
}
90142

@@ -131,6 +183,8 @@ const detectFromHeaders = (headers: Record<string, string>, url: string, headerR
131183
lowerHeaderBlob
132184
)
133185

186+
markSpoofedHeaderDetections(technologies, headers)
187+
134188
return technologies
135189
}
136190

0 commit comments

Comments
 (0)