Skip to content

Commit 08f344f

Browse files
committed
fix(review-feedback-1708): 落地可配置 RSS/Atom 情报源、存储、查询、retention 和基础安全边界
1 parent cfa1253 commit 08f344f

3 files changed

Lines changed: 28 additions & 12 deletions

File tree

docs/intelligence-sources.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ Issue #1707 的首版能力聚焦“合规资讯源采集、本地沉淀、可
1818

1919
- 只允许绝对 `http` / `https` URL;
2020
- 禁止 URL 中携带 username/password;
21-
- 禁止 `localhost``.local`、回环地址、内网地址、链路本地地址、保留地址和组播地址;
21+
- 禁止 `localhost``.local`、回环地址、内网地址、链路本地地址、保留地址、共享地址段和组播地址;
22+
- 解析与拉取阶段显式禁用环境代理(如 `HTTP_PROXY``HTTPS_PROXY``ALL_PROXY`),避免通过环境代理绕过校验边界;
2223
- 实际连接阶段会再次校验目标主机 DNS 解析结果,避免校验后解析漂移到受限地址;
2324
- 重定向后的最终 URL 也会再次校验;
2425
- 错误消息会脱敏常见 `token` / `key` / `secret` 查询参数。

src/services/intelligence_service.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
_MAX_REDIRECTS = 5
3333
_REDIRECT_STATUS_CODES = {301, 302, 303, 307, 308}
3434
_DNS_GUARD_LOCK = threading.RLock()
35+
_DISABLE_REQUEST_PROXIES = {"http": None, "https": None}
3536

3637

3738
class IntelligenceServiceError(ValueError):
@@ -248,7 +249,9 @@ def guarded_getaddrinfo(host: Any, port: Any, *args: Any, **inner_kwargs: Any) -
248249
with _DNS_GUARD_LOCK:
249250
socket.getaddrinfo = guarded_getaddrinfo
250251
try:
251-
return requests.get(raw_url, **kwargs)
252+
request_kwargs = dict(kwargs)
253+
request_kwargs.setdefault("proxies", _DISABLE_REQUEST_PROXIES)
254+
return requests.get(raw_url, **request_kwargs)
252255
finally:
253256
socket.getaddrinfo = original_getaddrinfo
254257

@@ -309,18 +312,11 @@ def _validate_addrinfos(addrinfos: Any) -> None:
309312
except ValueError as exc:
310313
raise IntelligenceServiceError("source url resolved to an invalid address") from exc
311314
if IntelligenceService._is_blocked_ip(ip):
312-
raise IntelligenceServiceError("source url must not target private or local network addresses")
315+
raise IntelligenceServiceError("source url must resolve to a public internet address")
313316

314317
@staticmethod
315318
def _is_blocked_ip(ip: ipaddress._BaseAddress) -> bool:
316-
return (
317-
ip.is_private
318-
or ip.is_loopback
319-
or ip.is_link_local
320-
or ip.is_reserved
321-
or ip.is_multicast
322-
or ip.is_unspecified
323-
)
319+
return not ip.is_global
324320

325321
def _parse_feed(self, content: bytes, *, source_name: str, limit: int) -> List[FeedEntry]:
326322
try:

tests/test_intelligence_service.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ def test_dns_name_resolving_private_address_is_rejected(self) -> None:
104104
with self.assertRaises(IntelligenceServiceError):
105105
self.service.create_source({"name": "bad", "url": "https://metadata.example.com/rss.xml", "scope_type": "market"})
106106

107+
def test_shared_address_space_url_is_rejected(self) -> None:
108+
shared_dns = [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("100.64.0.1", 0))]
109+
with patch("src.services.intelligence_service.socket.getaddrinfo", return_value=shared_dns):
110+
with self.assertRaises(IntelligenceServiceError):
111+
self.service.create_source({"name": "bad", "url": "https://metadata.example.com/rss.xml", "scope_type": "market"})
112+
107113
def test_generated_no_url_sentinel_is_rejected_for_source_url(self) -> None:
108114
with self.assertRaisesRegex(IntelligenceServiceError, "absolute http\\(s\\) URL"):
109115
self.service.create_source({"name": "bad", "url": "no-url:intel:anything", "scope_type": "market"})
@@ -124,8 +130,21 @@ def test_redirect_target_is_validated_before_following(self) -> None:
124130
with self._public_dns(), patch("src.services.intelligence_service.requests.get", return_value=redirect) as mock_get:
125131
with self.assertRaises(IntelligenceServiceError):
126132
self.service.fetch_source(source["id"])
133+
self.assertEqual(mock_get.call_count, 1)
134+
self.assertFalse(mock_get.call_args.kwargs["allow_redirects"])
135+
136+
def test_fetch_requests_disable_environment_proxies(self) -> None:
137+
with self._public_dns():
138+
source = self.service.create_source({
139+
"name": "proxy-safe-feed",
140+
"url": "https://feeds.example.com/rss.xml",
141+
"scope_type": "market",
142+
})
143+
with patch.dict(os.environ, {"HTTP_PROXY": "http://127.0.0.1:3128", "HTTPS_PROXY": "http://127.0.0.1:3128", "ALL_PROXY": "http://127.0.0.1:3128"}):
144+
with self._public_dns(), patch("src.services.intelligence_service.requests.get", return_value=self._mock_response()) as mock_get:
145+
self.service.fetch_source(source["id"])
127146
self.assertEqual(mock_get.call_count, 1)
128-
self.assertFalse(mock_get.call_args.kwargs["allow_redirects"])
147+
self.assertEqual(mock_get.call_args.kwargs["proxies"], {"http": None, "https": None})
129148

130149
def test_fetch_validates_dns_resolution_used_by_request(self) -> None:
131150
public_dns = [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", 0))]

0 commit comments

Comments
 (0)