项目:my-ts-axios | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
my-ts-axios 是一个 HTTP 客户端库,不是服务,它的"安全与权限设计"与应用系统不同:没有认证体系、没有授权模型、没有会话管理,它的安全面完全由四个问题构成——库把什么数据放进了请求(参数与头的注入面)、库把凭据发给了谁(credentials 与 CORS/重定向语义)、库如何处理不可信的响应(解析风险与资源护栏)、库自身引入了什么供应链风险(依赖面)。本篇按这四个问题组织,最后汇总一份"已知隐患与边界"清单。所有结论均以 src/ 源码为准,官方 axios 的对照只在其行为直接相关时提及。
先给一个全景判断:这个库的默认行为是保守偏放行的——decompress 默认 true、maxRedirects 默认 5、validateStatus 只放行 2xx、withCredentials 默认不开启、XSRF 头只在"同源或显式 withCredentials 且 cookie 存在"时附加;但在 URL/参数编码上它继承了官方 axios 的"白名单还原"风格,在响应解析上采取"失败静默"策略。理解每个默认值背后的取舍,是安全使用这个库的前提。
params 序列化的唯一实现在 src/helpers/url.ts 的 buildURL(第 1685 行),默认编码函数 13 行)是"先 encode(第 4encodeURIComponent,再把六个白名单字符还原":%40 → @、%3A → :、%24 → $、%2C → ,、%20 → +、%5B → [、%5D → ]。这个白名单与官方 axios 完全一致,目的是让 URL 更可读,但它有两层值得使用者知晓的安全语义:
第一,&、=、#、?、% 都不在白名单内,会被保留百分号编码,因此参数值无法通过序列化环节注入额外的查询参数或截断 URL——params: { a: '1&b=2' } 会编码为 a=1%26b%3D2,服务端只会看到一个键。这是白名单"只放行无结构含义字符"的正确设计。第二,空格被编码为 + 而非 %20:在 application/x-www-form-urlencoded 语境下 + 表示空格没有问题,但某些严格按 RFC 3986 解析查询串的服务端会把 + 当作字面加号。这不是漏洞,而是跨服务端兼容性需要留意的语义;若目标是严格 RFC 场景,可通过 paramsSerializer: { encode: v => encodeURIComponent(v) }(第 03 篇 3.1 节、src/types/index.ts 第 110 行)整体替换编码函数——这也意味着编码函数是完全由调用方控制的注入点:自定义 encode 若实现不当(例如直接拼接不做任何编码),注入风险由调用方引入,库不兜底。键名与值走同一个 encodeFn(url.ts 第 71 行),因此键名注入同样被默认编码覆盖。
buildURL 的拼接策略还有一个细节:先定位并剥离 url 中 # 及其后内容(第 77~79 行),再按 ?/& 追加。这防止了"params 拼接到 hash 后面变成无效参数"的意外,也顺带消除了"调用方在 url 里手工携带 fragment 导致参数失效"一类的小坑。
函数式 paramsSerializer(params) => string 返回的字符串原样拼接,不做任何校验或编码——这是设计使然(用户要求完全接管),但也意味着该入口的注入面完全取决于用户实现。对象形式的 serialize 同理(miscAlignment.spec.ts 验证了 serialize 优先于 encode/indexes 的次序)。baseURL 拼接(src/core/dispatchRequest.ts 的 transformURL,第 3037 行)中 93 行)只做斜杠规整,不解析、不校验;combineURL(url.ts 第 91isAbsoluteURL 的正则(第 87~89 行)接受任意 scheme:// 或 // 开头的 URL——//evil.com 会被判定为绝对地址而跳过 baseURL 拼接,若调用方把用户输入直接作为 url 传入,协议相对地址可以绕过 baseURL 约束打到任意外域。这不是库的缺陷(官方行为相同),但在"baseURL 是安全边界"的架构假设下必须由上层兜底(校验最终 URL 的 host,或用响应拦截器断言)。
三个适配器设置请求头的路径分别是:xhr setRequestHeader(浏览器内建禁止换行等非法字符)、fetch init.headers 普通对象、http 适配器 options.headers(Node 的 http.request 会对头值做合法性校验,非法字符抛错)。库自身不对头值做任何转义——头值来自 config.headers(用户输入)与适配器的程序化写入(XSRF 头、Authorization),后者只写 cookie 读出的值与 base64 结果,不存在拼接用户输入的路径。唯一的间接用户输入是 helpers/cookie.ts 的 read(name):正则 new RegExp('(^|;\\s*)(' + name + ')=([^;]*)') 直接内插 name,name 未做正则元字符转义。name 来自 config.xsrfCookieName,正常使用是常量,风险仅存在于"把不可信输入动态传入 xsrfCookieName"的非典型用法(后果是该次请求的 cookie 读取行为异常,最坏是构造糟糕正则导致的回溯开销)。属于"文档级提醒"而非需要修复的缺陷。
Node 端 URL 解析在 src/core/adapters/http.ts 的 _send(第 124~130 行)用 new URL 严格解析,失败即 ERR_INVALID_URL reject——解析器层面的 URL 注入(如控制字符绕过)由 WHATWG URL 标准本身兜底,库不额外处理。
config.withCredentials 只在 xhr 与 fetch 两个适配器消费:xhr 直接赋 request.withCredentials(xhr.ts 第 5860 行),fetch 映射为 186 行)。默认值是 init.credentials = 'include'(fetch.ts 第 184undefined——即跨域请求默认不带 cookie,这是浏览器同源策略之下的安全默认。withCredentials: true 一旦开启,服务端的 CORS 响应必须显式放行 origin 与凭据(不能是 *),这一约束由浏览器执行,库不参与。
同源判定由 isURLSameOrigin(url.ts 第 104111 行)提供,浏览器内用 107 行注释:"Node 无法做同源判断,默认视为同源")。这个默认值的安全含义要在 XSRF 一节展开——它决定了 Node 端不会做 XSRF 附加,而不是"Node 端真的都是同源"。<a> 标签解析 protocol+host 对比;非浏览器环境恒返回 true(第 105
http 适配器的凭据有三个来源:config.auth(拼入 options.auth,Node 底层转 Authorization 头)、headers 中显式的 Authorization/Cookie、代理场景的 proxy.auth(转 Proxy-Authorization)。安全设计的核心在第 136143 行:226 行:stripAuth 为真时删除 authorization、proxy-authorization、cookie 三个键,而 stripAuth 在跨 host 重定向时被置位(第 224new URL(nextUrl).host !== parsed.host → _send(..., stripAuth || crossHost)),且一旦剥离后续所有跳保持剥离。这是对齐 follow-redirects 的防凭据泄露设计,测试锁定在 test/core/httpAdapterHardening.spec.ts 的"同 host 保留 / 跨 host 剥离"两条用例(服务端回显 authorization 与 cookie,断言跨 host 后为 null)。注意剥离的粒度是"host 不同"(含端口不同),且只作用于重定向链——首次请求的凭据永远发往用户指定的 URL,库无法也不应阻止。
一个值得知道的边界:config.auth 在 Node 端通过 options.auth 交给 http 模块处理,Node 会将其编入 Authorization: Basic 头;浏览器端则是 btoa(auth.username + ':' + auth.password)(xhr.ts 第 152 行)。btoa 对非 Latin1 字符(如中文用户名)会抛 InvalidCharacterError,fetch 适配器的 base64 助手(fetch.ts 第 10~15 行)只在 btoa 不存在时回退 Buffer,不捕获 InvalidCharacterError——这是三端在非 ASCII 凭据下行为不一致的边界(xhr/fetch 抛错,Node 正常),需要调用方自行先做 UTF-8 安全编码。
以源码为准:本库实现了与官方 axios 相同机制的 XSRF 防护,落在 xhr 与 fetch 两个适配器;http 适配器不适用(服务端请求没有浏览器 cookie 语境)。实现点:xhr.ts 第 140150 行与 171 行,逻辑一致——fetch.ts 第 157
withXSRFToken 为函数时以其返回值为准;否则 withXSRFToken !== false(即未配置时视为允许)。fetch 端额外要求 typeof document !== 'undefined'。withCredentials || isURLSameOrigin(url)——只有同源请求或显式带凭据的请求才附加(跨域且不带凭据时附加 token 反而可能把 token 泄露给第三方域,这是官方同款设计)。cookie.read(config.xsrfCookieName) 读 cookie(默认名 XSRF-TOKEN,src/defaults.ts 第 21 行),有值且 xsrfHeaderName(默认 X-XSRF-TOKEN)存在时写入请求头。需要澄清的边界有三条。其一,防护模型是"cookie-to-header 映射":它假设服务端把 CSRF token 种在 cookie 里,前端读出来放进自定义头,服务端比对头与 cookie。库只负责"读 cookie → 放头",token 的生成、种置与校验全部在服务端。其二,withXSRFToken 的默认语义是"未配置即允许"(非 false 即真),比"未配置即禁止"更宽松;要严格控制在哪些请求附加 token,应传函数形式 withXSRFToken: config => ...。其三,cookie.read 只能读非 HttpOnly cookie——这恰是该机制的前提(HttpOnly cookie 本身就自带 CSRF 防护,无需此机制)。测试覆盖见 test/xsrf.spec.ts(cookie 缺失不附头 / cookie 存在附头)与 test/advancedConfig.spec.ts 的 withXSRFToken 分支。
默认 transformResponse(src/helpers/data.ts 第 1524 行)只对字符串尝试 48 行)——为 false 时整个 transformResponse 管道被跳过,响应保持原始形态。注意这与官方 axios 的语义粒度不同(官方 forcedJSONParsing=false 仍保留 text/responseType=json 的部分行为),详见第 03 篇差异清单第 5 条。JSON.parse,失败静默保持字符串(catch 空块)。这带来两个安全相关的事实:第一,解析失败不会暴露为错误,调用方若假设 response.data 一定是对象,可能把字符串当作对象使用而触发下游 TypeError——防御方式是在拦截器里做形态断言;第二,JSON.parse 本身不会造成原型污染(JSON.parse 以 DefineOwnProperty 语义创建 __proto__ 自有属性,不触发 setter),因此"服务端返回恶意 JSON"不会通过默认转换器污染原型。transitional.silentJSONParsing 声明为 true 但全库无消费点(第 13 篇条目);实际控制解析开关的是 transitional.forcedJSONParsing === false(dispatchRequest.ts 第 39
responseType 决定原始 data 形态(完整矩阵见《04-数据模型》第 3 节):arraybuffer/blob 原样返回不解析;stream 在 Node 端返回 Readable 流(http.ts 第 252256 行)、在 fetch 端返回原始 ReadableStream(292 行)。流形态的数据绕过所有内容长度护栏(流边读边消费,库不再统计),内存与下载量的控制责任转移到调用方。非流形态下,http 适配器把整个响应累积进内存(fetch.ts 第 291chunks 数组 + Buffer.concat),护栏是 maxContentLength(http.ts 第 264~273 行):每个 chunk 到达即校验累计长度,超限 stream.destroy() 并 reject——由于校验发生在解压后的流上(buildDecompressStream 在第 259 行先于累积执行),它同时就是解压炸弹(decompression bomb)的部分护栏:一个 1MB 的 gzip 可以解压成数 GB,但累计超过 maxContentLength 即中止,不会把内存耗尽。护栏只在显式配置时生效(config.maxContentLength != null 判断),默认不设限,高风险场景应显式配置。
decompress 默认 true(http.ts 第 258 行 config.decompress !== false)还有一层安全语义:解压后响应体的 content-length 与实际长度不再对应,依赖 content-length 做完整性校验的下游逻辑需要在解压前介入(本库的下载进度流正是设计在解压前统计原始字节,http.ts 第 239~250 行)。
运行时依赖面在同类库中属于最小化:零硬依赖,Node 内置模块由运行时提供,form-data 是 optionalDependencies(package.json 第 113115 行),缺失时代码降级(29 行 try/catch require),不存在"可选依赖被替换为恶意版本"的强攻击面——但如果安装了 http.ts 第 24form-data,其版本固定在 ^4.0.0,供应链风险随该包走。锁文件 package-lock.json 入库保证可复现安装。
开发依赖面较大(约 40 个,含构建/测试/示例/规范四组,见《06-部署运行》第 1 节),全部只影响开发环境不进入产物,但其中 Vite/Express/rollup 等都是历史漏洞的高发带。已核实的工程事实:.github/workflows/ci.yml 存在且包含 lint → test → build → examples:build 四步(Node 20/22 矩阵),但没有依赖审计步骤(无 npm audit/osv-scanner/dependabot 配置)。建议的补强是:CI 中加 npm audit --audit-level=high(开发依赖可容忍 low/medium),并考虑 dependabot/renovate 自动升级 PR。产物本身的供应链面由"零依赖 + files: ['dist'] + smoke-dist 冒烟"三者共同收敛,这一点设计得比多数学习项目严格。
以下是逐条核对源码后确认的隐患/不一致项,多数已在其他篇目以"差异清单"或"待确认"形式出现,本节从安全与健壮性视角集中陈列并给出处置建议:
| 条目 | 位置 | 安全/健壮性影响 | 处置建议 |
|---|---|---|---|
ERR_PROXY 为自增错误码 | src/helpers/error.ts 第 16 行;http.ts 第 486、524 行 | 非安全缺陷,但依赖 error.code 分流的容错代码可能漏判这个官方不存在的码 | 若坚持对齐官方可改用普通 Error;至少在文档显式登记(已在《03》第 4 节、《13》条目化) |
transitional.silentJSONParsing 声明未消费 | src/defaults.ts 第 70 行、src/types/index.ts 第 134 行 | 调用方以为可以关闭"静默 JSON 解析失败",实际配置无效,失败依然静默 | 二选一:消费它(在 transformResponse 里失败时抛 ERR_BAD_RESPONSE),或从类型与 defaults 中删除并在文档标注 |
_requestForm 绕过 defaults.env.FormData | src/core/Axios.ts 第 193 行(toFormData(data, undefined, ...)),src/helpers/formData.ts 第 22 行回退全局 new FormData() | 非 browser/Node 全局 FormData 的注入环境(如自定义 polyfill、测试替身)下,postForm 系与默认转换器行为不一致 | 改为 toFormData(data, undefined, options) 前先从 this.defaults.env?.FormData 构造 form 实例,与 defaults.ts 第 42~44 行对齐 |
btoa 非 Latin1 凭据抛错 | xhr.ts 第 152 行;fetch.ts 第 10~15 行 | 中文/emoji 用户名密码在浏览器端抛 InvalidCharacterError(非 AxiosError),Node 端正常——三端不一致 | 统一先 unescape(encodeURIComponent(s)) 或 TextEncoder+逐字节 btoa 预处理 |
formToJSON 的原型语义 | src/helpers/formData.ts 第 140~163 行 | assignDeep 用自有属性检查防了 in 的原型链误命中(注释明确),但 cur[seg] = ... 在 seg 恰为 __proto__ 时仍走 setter:形如 __proto__[x] 的键会替换结果对象自身的原型(构造 obj['__proto__'] = {} 再向其写入),未发现污染 Object.prototype 的全局路径 ⚠️ 待确认(中间层 hasOwn 检查阻断了 constructor.prototype 链) | 防御性修复:`seg === 'proto' |
| 默认无超时 | src/defaults.ts 第 19 行 timeout: 0 | 请求可能无限期挂起(慢响应、半开连接),Node 端并发场景会累积悬挂 Promise 与 socket | 生产环境在实例 defaults 统一设置合理 timeout;http 端注意超时是 socket 空闲计时(《05》流程三) |
insecureHTTPParser 透传 | src/types/index.ts 第 83 行、http.ts 第 164 行 | 打开它等于放宽 Node 的 HTTP 解析严格性(接受畸形请求行/头),仅在对接遗留服务时需要 | 保持默认关闭;确需开启时限制在受控内网实例上 |
| fetch 无上传进度 | fetch.ts(onUploadProgress 未消费) | 上传大文件时无法观测进度,也不存在"上传卡死可观测"手段 | fetch API 限制(可用双 body 技巧,复杂度高不值得);上传进度敏感场景选 xhr/http 适配器 |
cookie.read 正则未转义 | src/helpers/cookie.ts 第 3 行 | 仅当 xsrfCookieName 来自不可信输入时有异常/回溯风险 | 若要加固:`name.replace(/[.*+?^${}() |
把本篇收敛为三句话:发送侧,默认编码白名单与严格 URL 解析把注入面压到"自定义 paramsSerializer/encode"这个显式托管的入口,// 开头的绝对地址绕过 baseURL 是调用方必须自知的边界;凭据侧,浏览器默认不带凭据、Node 跨 host 重定向强制剥离凭据、XSRF 防护按"同源或显式凭据"条件附加,三条默认值都处在安全侧;接收侧,JSON 解析静默失败与默认不限内容长度是两个最值得生产环境显式收紧的点(拦截器断言 data 形态、配置 maxContentLength/timeout)。所有"已知隐患"都不阻塞当前学习/使用定位,但若走向生产发布,第 7 节清单即是整改工单。