@@ -56,6 +56,43 @@ const DEFAULT_CONFIG: OllamaConfig = {
5656 registryMirror : '' ,
5757} ;
5858
59+ // ================================================================
60+ // baseUrl 安全校验(SSRF 防护)
61+ // ================================================================
62+
63+ /**
64+ * 校验 Ollama baseUrl — 仅允许本机回环地址,拒绝其他所有主机名/IP。
65+ * 防止渲染进程把 baseUrl 指向内网或公网地址(SSRF 攻击面)。
66+ * 允许:http://localhost:* / http://127.0.0.1:* / http://[::1]:*
67+ * (含 WHATWG URL 规范化的 IPv4-mapped 形式 ::ffff:127.0.0.1)
68+ * @returns 违规原因;null 表示通过 / Return block reason or null
69+ */
70+ export function validateOllamaBaseUrl ( rawUrl : string ) : string | null {
71+ let parsed : URL ;
72+ try {
73+ parsed = new URL ( rawUrl ) ;
74+ } catch {
75+ return 'URL 格式无效' ;
76+ }
77+ if ( parsed . protocol !== 'http:' ) {
78+ return '仅支持 http:// 协议' ;
79+ }
80+ // hostname 已去除 [],IPv6 形如 ::1,IPv4-mapped 形如 ::ffff:127.0.0.1
81+ const host = parsed . hostname . toLowerCase ( ) . replace ( / ^ \[ | \] $ / g, '' ) ;
82+ const isLoopback =
83+ host === 'localhost' ||
84+ host === '127.0.0.1' ||
85+ host === '::1' ||
86+ host === '0:0:0:0:0:0:0:1' ||
87+ // IPv4-mapped IPv6(::ffff:127.0.0.1)规范化后必须一并放行(仍指向本机)
88+ host === '::ffff:127.0.0.1' ||
89+ host === '::ffff:7f00:1' ;
90+ if ( ! isLoopback ) {
91+ return '仅允许连接本机 Ollama 服务(localhost / 127.0.0.1 / [::1])' ;
92+ }
93+ return null ;
94+ }
95+
5996// ================================================================
6097// 运行时状态
6198// ================================================================
@@ -91,7 +128,9 @@ export async function loadOllamaConfig(): Promise<void> {
91128 const parsed = JSON . parse ( raw ) ;
92129 _config = {
93130 enabled : typeof parsed . enabled === 'boolean' ? parsed . enabled : DEFAULT_CONFIG . enabled ,
131+ // SEC: 持久化文件中的 baseUrl 同样校验——防止历史/被篡改配置绕过 SSRF 防护
94132 baseUrl : typeof parsed . baseUrl === 'string' && parsed . baseUrl . trim ( )
133+ && validateOllamaBaseUrl ( parsed . baseUrl . trim ( ) ) === null
95134 ? parsed . baseUrl . trim ( ) . replace ( / \/ $ / , '' )
96135 : DEFAULT_CONFIG . baseUrl ,
97136 models : {
@@ -115,11 +154,19 @@ export async function loadOllamaConfig(): Promise<void> {
115154 */
116155export async function updateOllamaConfig ( partial : Partial < OllamaConfig > ) : Promise < OllamaConfig > {
117156 const current = getOllamaConfig ( ) ;
157+ // SEC: baseUrl SSRF 校验——仅允许本机回环地址,违规直接拒绝更新
158+ let nextBaseUrl = current . baseUrl ;
159+ if ( partial . baseUrl ) {
160+ const trimmed = partial . baseUrl . trim ( ) . replace ( / \/ $ / , '' ) ;
161+ const blockReason = validateOllamaBaseUrl ( trimmed ) ;
162+ if ( blockReason ) {
163+ throw new Error ( `[Ollama] baseUrl 校验失败:${ blockReason } ` ) ;
164+ }
165+ nextBaseUrl = trimmed ;
166+ }
118167 const updated : OllamaConfig = {
119168 enabled : partial . enabled ?? current . enabled ,
120- baseUrl : partial . baseUrl
121- ? partial . baseUrl . trim ( ) . replace ( / \/ $ / , '' )
122- : current . baseUrl ,
169+ baseUrl : nextBaseUrl ,
123170 models : {
124171 text : partial . models ?. text ?? current . models . text ,
125172 vision : partial . models ?. vision ?? current . models . vision ,
0 commit comments