偶然探究httpx的链子,关于伪造data块的可能性

admin 2026-04-25 04:29:45 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文分析了httpx库中参数传递机制的安全风险,重点探讨了data与json参数共存时可能存在的混淆漏洞。作者通过跟踪源码发现参数边界处理不当可能导致伪造数据块的风险,并建议开发者注意参数规范对代码安全性的影响。文章提供了具体的代码审计方法和安全开发建议。 综合评分: 72 文章分类: 代码审计,漏洞分析,WEB安全,安全开发,其他


cover_image

偶然探究httpx的链子,关于伪造data块的可能性

原创

YMsora YMsora

YMs0ra的安全漫路

2026年3月26日 21:15 浙江

在小说阅读器读本章

去阅读

因为一些原因重新去看了一个题的源码,算是时隔好久了

核心在于httpx。正常把库扒下来。接下来跟一根乱七八糟的api和参数

很多时候是因为参数更迭的混乱导致审计的困难,

但是好像随着时间的流逝,这点也慢慢学会了。

有时候就是什么问题就一直想着看源码,忽视了很多搜集信息的渠道

得稍微摆正自己的思维了

简单跟一下源码,来吧

我们先看看最前面的api逻辑。

def request(    method: str,    url: URL | str,    *,    params: QueryParamTypes | None = None,    content: RequestContent | None = None,    data: RequestData | None = None,    files: RequestFiles | None = None,    json: typing.Any | None = None,    headers: HeaderTypes | None = None,    cookies: CookieTypes | None = None,    auth: AuthTypes | None = None,    proxy: ProxyTypes | None = None,    timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,    follow_redirects: bool = False,    verify: ssl.SSLContext | str | bool = True,    trust_env: bool = True,) -> Response:    """    Sends an HTTP request.
    **Parameters:**
    * **method** - HTTP method for the new `Request` object: `GET`, `OPTIONS`,    `HEAD`, `POST`, `PUT`, `PATCH`, or `DELETE`.    * **url** - URL for the new `Request` object.    * **params** - *(optional)* Query parameters to include in the URL, as a    string, dictionary, or sequence of two-tuples.    * **content** - *(optional)* Binary content to include in the body of the    request, as bytes or a byte iterator.    * **data** - *(optional)* Form data to include in the body of the request,    as a dictionary.    * **files** - *(optional)* A dictionary of upload files to include in the    body of the request.    * **json** - *(optional)* A JSON serializable object to include in the body    of the request.    * **headers** - *(optional)* Dictionary of HTTP headers to include in the    request.    * **cookies** - *(optional)* Dictionary of Cookie items to include in the    request.    * **auth** - *(optional)* An authentication class to use when sending the    request.    * **proxy** - *(optional)* A proxy URL where all the traffic should be routed.    * **timeout** - *(optional)* The timeout configuration to use when sending    the request.    * **follow_redirects** - *(optional)* Enables or disables HTTP redirects.    * **verify** - *(optional)* Either `True` to use an SSL context with the    default CA bundle, `False` to disable verification, or an instance of    `ssl.SSLContext` to use a custom context.    * **trust_env** - *(optional)* Enables or disables usage of environment    variables for configuration.
    **Returns:** `Response`
    Usage:
&nbsp; &nbsp;&nbsp;```&nbsp; &nbsp; >>> import httpx&nbsp; &nbsp; >>> response = httpx.request('GET', 'https://httpbin.org/get')&nbsp; &nbsp; >>> response&nbsp; &nbsp; <Response [200 OK]>&nbsp; &nbsp; ```&nbsp; &nbsp; """&nbsp; &nbsp; with Client(&nbsp; &nbsp; &nbsp; &nbsp; cookies=cookies,&nbsp; &nbsp; &nbsp; &nbsp; proxy=proxy,&nbsp; &nbsp; &nbsp; &nbsp; verify=verify,&nbsp; &nbsp; &nbsp; &nbsp; timeout=timeout,&nbsp; &nbsp; &nbsp; &nbsp; trust_env=trust_env,&nbsp; &nbsp; ) as client:&nbsp; &nbsp; &nbsp; &nbsp; return client.request(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; method=method,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; url=url,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content=content,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; data=data,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; files=files,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; json=json,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; params=params,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; headers=headers,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; auth=auth,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; follow_redirects=follow_redirects,&nbsp; &nbsp; &nbsp; &nbsp; )

虽然初见参数很多,而且前面引入了其他的模块,先且不论,这里也就是正常规定了一个response

跟着看看client的request方法,接收参数形式倒是默认的

因为接收的参数很多,所以有时候会想多参数引用的边界,参数的规范真的对于代码可读性有非常大的影响

继续跟进

def&nbsp;request(&nbsp; &nbsp; self,&nbsp; &nbsp; method:&nbsp;str,&nbsp; &nbsp; url: URL |&nbsp;str,&nbsp; &nbsp; *,&nbsp; &nbsp; content: RequestContent |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; data: RequestData |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; files: RequestFiles |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; json: typing.Any&nbsp;|&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; params: QueryParamTypes |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; headers: HeaderTypes |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; cookies: CookieTypes |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; auth: AuthTypes | UseClientDefault |&nbsp;None&nbsp;= USE_CLIENT_DEFAULT,&nbsp; &nbsp; follow_redirects:&nbsp;bool&nbsp;| UseClientDefault = USE_CLIENT_DEFAULT,&nbsp; &nbsp; timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,&nbsp; &nbsp; extensions: RequestExtensions |&nbsp;None&nbsp;=&nbsp;None,) -> Response:&nbsp; &nbsp;&nbsp;"""&nbsp; &nbsp; Build and send a request.
&nbsp; &nbsp; Equivalent to:
&nbsp; &nbsp; ```python&nbsp; &nbsp; request = client.build_request(...)&nbsp; &nbsp; response = client.send(request, ...)&nbsp; &nbsp; ```
&nbsp; &nbsp; See `Client.build_request()`, `Client.send()` and&nbsp; &nbsp; [Merging of configuration][0] for how the various parameters&nbsp; &nbsp; are merged with client-level configuration.
&nbsp; &nbsp; [0]: /advanced/clients/#merging-of-configuration&nbsp; &nbsp; """&nbsp; &nbsp;&nbsp;if&nbsp;cookies&nbsp;is&nbsp;not&nbsp;None:&nbsp; &nbsp; &nbsp; &nbsp; message = (&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"Setting per-request cookies=<...> is being deprecated, because "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"the expected behaviour on cookie persistence is ambiguous. Set "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"cookies directly on the client instance instead."&nbsp; &nbsp; &nbsp; &nbsp; )&nbsp; &nbsp; &nbsp; &nbsp; warnings.warn(message, DeprecationWarning, stacklevel=2)
&nbsp; &nbsp; request =&nbsp;self.build_request(&nbsp; &nbsp; &nbsp; &nbsp; method=method,&nbsp; &nbsp; &nbsp; &nbsp; url=url,&nbsp; &nbsp; &nbsp; &nbsp; content=content,&nbsp; &nbsp; &nbsp; &nbsp; data=data,&nbsp; &nbsp; &nbsp; &nbsp; files=files,&nbsp; &nbsp; &nbsp; &nbsp; json=json,&nbsp; &nbsp; &nbsp; &nbsp; params=params,&nbsp; &nbsp; &nbsp; &nbsp; headers=headers,&nbsp; &nbsp; &nbsp; &nbsp; cookies=cookies,&nbsp; &nbsp; &nbsp; &nbsp; timeout=timeout,&nbsp; &nbsp; &nbsp; &nbsp; extensions=extensions,&nbsp; &nbsp; )&nbsp; &nbsp;&nbsp;return&nbsp;self.send(request, auth=auth, follow_redirects=follow_redirects)

没啥都,直接看build_request

def&nbsp;build_request(&nbsp; &nbsp; self,&nbsp; &nbsp; method:&nbsp;str,&nbsp; &nbsp; url: URL |&nbsp;str,&nbsp; &nbsp; *,&nbsp; &nbsp; content: RequestContent |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; data: RequestData |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; files: RequestFiles |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; json: typing.Any&nbsp;|&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; params: QueryParamTypes |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; headers: HeaderTypes |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; cookies: CookieTypes |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,&nbsp; &nbsp; extensions: RequestExtensions |&nbsp;None&nbsp;=&nbsp;None,) -> Request:&nbsp; &nbsp;&nbsp;"""&nbsp; &nbsp; Build and return a request instance.
&nbsp; &nbsp; * The `params`, `headers` and `cookies` arguments&nbsp; &nbsp; are merged with any values set on the client.&nbsp; &nbsp; * The `url` argument is merged with any `base_url` set on the client.
&nbsp; &nbsp; See also: [Request instances][0]
&nbsp; &nbsp; [0]: /advanced/clients/#request-instances&nbsp; &nbsp; """&nbsp; &nbsp; url =&nbsp;self._merge_url(url)&nbsp; &nbsp; headers =&nbsp;self._merge_headers(headers)&nbsp; &nbsp; cookies =&nbsp;self._merge_cookies(cookies)&nbsp; &nbsp; params =&nbsp;self._merge_queryparams(params)&nbsp; &nbsp; extensions = {}&nbsp;if&nbsp;extensions&nbsp;is&nbsp;None&nbsp;else&nbsp;extensions&nbsp; &nbsp;&nbsp;if&nbsp;"timeout"&nbsp;not&nbsp;in&nbsp;extensions:&nbsp; &nbsp; &nbsp; &nbsp; timeout = (&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.timeout&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;isinstance(timeout, UseClientDefault)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else&nbsp;Timeout(timeout)&nbsp; &nbsp; &nbsp; &nbsp; )&nbsp; &nbsp; &nbsp; &nbsp; extensions =&nbsp;dict(**extensions, timeout=timeout.as_dict())&nbsp; &nbsp;&nbsp;return&nbsp;Request(&nbsp; &nbsp; &nbsp; &nbsp; method,&nbsp; &nbsp; &nbsp; &nbsp; url,&nbsp; &nbsp; &nbsp; &nbsp; content=content,&nbsp; &nbsp; &nbsp; &nbsp; data=data,&nbsp; &nbsp; &nbsp; &nbsp; files=files,&nbsp; &nbsp; &nbsp; &nbsp; json=json,&nbsp; &nbsp; &nbsp; &nbsp; params=params,&nbsp; &nbsp; &nbsp; &nbsp; headers=headers,&nbsp; &nbsp; &nbsp; &nbsp; cookies=cookies,&nbsp; &nbsp; &nbsp; &nbsp; extensions=extensions,&nbsp; &nbsp; )

我们可以看到_merge_queryparams和_merge_cookies,_merge_headers以及_merge_url 可以看到最后的extension,去验证了看看是否有扩展有危险拼入,果然不出所料,没有,继续追踪

class&nbsp;Request:&nbsp; &nbsp;&nbsp;def&nbsp;__init__(&nbsp; &nbsp; &nbsp; &nbsp; self,&nbsp; &nbsp; &nbsp; &nbsp; method:&nbsp;str,&nbsp; &nbsp; &nbsp; &nbsp; url: URL |&nbsp;str,&nbsp; &nbsp; &nbsp; &nbsp; *,&nbsp; &nbsp; &nbsp; &nbsp; params: QueryParamTypes |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; &nbsp; &nbsp; headers: HeaderTypes |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; &nbsp; &nbsp; cookies: CookieTypes |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; &nbsp; &nbsp; content: RequestContent |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; &nbsp; &nbsp; data: RequestData |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; &nbsp; &nbsp; files: RequestFiles |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; &nbsp; &nbsp; json: typing.Any&nbsp;|&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; &nbsp; &nbsp; stream: SyncByteStream | AsyncByteStream |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; &nbsp; &nbsp; extensions: RequestExtensions |&nbsp;None&nbsp;=&nbsp;None,&nbsp; &nbsp; ) ->&nbsp;None:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.method = method.upper()&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.url = URL(url)&nbsp;if&nbsp;params&nbsp;is&nbsp;None&nbsp;else&nbsp;URL(url, params=params)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.headers = Headers(headers)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.extensions = {}&nbsp;if&nbsp;extensions&nbsp;is&nbsp;None&nbsp;else&nbsp;dict(extensions)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;cookies:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Cookies(cookies).set_cookie_header(self)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;stream&nbsp;is&nbsp;None:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content_type:&nbsp;str&nbsp;|&nbsp;None&nbsp;=&nbsp;self.headers.get("content-type")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; headers, stream = encode_request(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content=content,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; data=data,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; files=files,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; json=json,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; boundary=get_multipart_boundary_from_content_type(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content_type=content_type.encode(self.headers.encoding)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;content_type&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else&nbsp;None&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self._prepare(headers)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.stream = stream&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# Load the request body, except for streaming content.&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;isinstance(stream, ByteStream):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.read()&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# There's an important distinction between `Request(content=...)`,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# and `Request(stream=...)`.&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;#&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# Using `content=...` implies automatically populated `Host` and content&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# headers, of either `Content-Length: ...` or `Transfer-Encoding: chunked`.&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;#&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# Using `stream=...` will not automatically include *any*&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# auto-populated headers.&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;#&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# As an end-user you don't really need `stream=...`. It's only&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# useful when:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;#&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# * Preserving the request stream when copying requests, eg for redirects.&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# * Creating request instances on the *server-side* of the transport API.&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.stream = stream
&nbsp;boundary=get_multipart_boundary_from_content_type(&nbsp;content_type=content_type.encode(self.headers.encoding)&nbsp;if&nbsp;content_type&nbsp;else&nbsp;None

这里的boundary分块可以被ct头指定,也就是说我们可以通过掌控CT头去增加form-data块

然后我们看回题目源码

&nbsp; action = request.files.get("action")&nbsp; act = json.loads(action.stream.read().decode())&nbsp;&nbsp;if&nbsp;act["type"] ==&nbsp;"echo":&nbsp;&nbsp; &nbsp; &nbsp;return&nbsp;content,&nbsp;200&nbsp; elif act["type"] ==&nbsp;"debug":&nbsp; &nbsp; &nbsp;return&nbsp;content.format(app),&nbsp;200&nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp;return&nbsp;'unkown action',&nbsp;400

可以看到读取了form-data的action,注意是file.get

act也就是action的数据流,如果里面的type的key是debug

就直接渲染app的上下文。我们也就是直接把context上下文渲染走format路线

然后后置的路线也就是我们控制了通过拿到了global的apikey,然后

就能访问admin去执行jinjia模板渲染

于是利用传输大于500kb的文件去达到写入临时文件,然后就可以加载jinjia模板了

当然我认为重要的点在前置。

OK了,exp可以在先知社区参考,这里就不放出了


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:YMs0ra的安全漫路 YMsora YMsora《偶然探究httpx的链子,关于伪造data块的可能性》

评论:0   参与:  0