accounting/wwwroot/api/fetch_forex_rates.dspy
yumoqing 311753689a security: fetch_forex_rates.dspy 加 localhost 校验(定时任务入口)
该端点授权给 any 且无任何鉴权,会发起外网抓取(BOC 页面)并写 exchange_rate 表,
匿名可反复触发 → 外部请求放大 + 脏数据写入 + DoS。改为仅本机可调(client_ip)。
build.sh 的 crontab 本就是 curl localhost:9090,不影响定时任务。
2026-08-25 15:01:21 +08:00

74 lines
3.1 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 获取中国银行外汇牌价并插入 exchange_rate 表
# GET /accounting/api/fetch_forex_rates.dspy
#
# 安全:本端点是定时任务入口(会发起外网抓取 + 写库),只允许本机调用。
# 之前无任何鉴权且授权给 any 角色,匿名可反复触发 → 外部抓取放大 + 脏数据写入 + DoS。
# 判定依据 client_ipnginx 用 $proxy_add_x_forwarded_for 追加模式、中间件取链尾值,
# 外部伪造 X-Forwarded-For 会被追加真实 IP 到链尾,伪造不成立;且应用端口不对外开放。
_ip = request.get('client_ip') or ''
if _ip not in ('127.0.0.1', '::1', 'localhost'):
return json.dumps({'success': False, 'message': '仅允许本机调用(定时任务入口)'},
ensure_ascii=False)
import re
import urllib.request
from datetime import date
BOC_URL = 'https://www.boc.cn/sourcedb/whpj/index.html'
CURRENCIES = {'美元': 'USD', '日元': 'JPY', '英镑': 'GBP'}
async with get_sor_context(request._run_ns, 'accounting') as sor:
# 1. 抓取 BOC 页面
try:
req = urllib.request.Request(BOC_URL, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req, timeout=15) as resp:
html = resp.read().decode('utf-8', errors='replace')
except Exception as e:
return json.dumps({'success': False, 'message': f'获取BOC页面失败: {e}'}, ensure_ascii=False)
today = date.today().isoformat()
inserted = 0
for cn_name, code in CURRENCIES.items():
pos = html.find(cn_name)
if pos < 0:
continue
chunk = html[pos:pos+500]
tds = re.findall(r'<td[^>]*>(.*?)</td>', chunk, re.DOTALL)
clean = [re.sub(r'<[^>]+>', '', t).strip() for t in tds]
if len(clean) < 6:
continue
try:
buy_rate = float(clean[1]) # 现汇买入价
sell_rate = float(clean[3]) # 现汇卖出价
mid_rate = float(clean[5]) # 中行折算价
except ValueError:
continue
# BOC quotes USD/GBP per 100 units, JPY per 100 units
if code in ('USD', 'GBP'):
buy_rate = round(buy_rate / 100, 6)
sell_rate = round(sell_rate / 100, 6)
mid_rate = round(mid_rate / 100, 6)
elif code == 'JPY':
buy_rate = round(buy_rate / 100, 6)
sell_rate = round(sell_rate / 100, 6)
mid_rate = round(mid_rate / 100, 6)
record_id = f'er_{code.lower()}_cny_{today.replace("-","")}'
await sor.execute(
"""INSERT INTO exchange_rate (id, from_currency, to_currency, buy_rate, sell_rate, mid_rate, effective_date)
VALUES (${id}$, ${from_cur}$, 'CNY', ${buy}$, ${sell}$, ${mid}$, ${date}$)
ON DUPLICATE KEY UPDATE buy_rate=${buy}$, sell_rate=${sell}$, mid_rate=${mid}$""",
{'id': record_id, 'from_cur': code, 'buy': buy_rate, 'sell': sell_rate, 'mid': mid_rate, 'date': today}
)
inserted += 1
return json.dumps({
'success': True,
'message': f'已更新 {inserted} 条汇率',
'date': today,
'source': '中国银行外汇牌价'
}, ensure_ascii=False)