Skip to content

export-webhook: a userinfo in the URL is sent as authorization: Basic, as 1.5.5 sent it (audit export-webhook #1, HIGH) - #19

Open
MattJackson wants to merge 1 commit into
devfrom
audit-userinfo-basic
Open

MattJackson wants to merge 1 commit into
devfrom
audit-userinfo-basic

Conversation

@MattJackson

@MattJackson MattJackson commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Audit pinned export-webhook finding #1 (HIGH), ARCHITECT ruling condition (c).

What. A webhook URL carrying a userinfo (https://user:pass@host/path) is sent as 1.5.5 sent it: authorization: Basic <base64(user:pass)> on every POST.

Why. THE DESIGN §2, "A port keeps 1.5.5's surface ... the default is 1.5.5's behaviour". 1.5.5 POSTed through reqwest 0.12. Its RequestBuilder::new called extract_authority (reqwest-0.12.28 src/async_impl/request.rs:169-183, :581-605), which took the URL's username and password off the URL and appended basic_auth (src/util.rs:4-25). After the port, the sink sent no credential, and the host refused the need's userinfo-carrying target, so the sink was disabled.

How (export-webhook/src/lib.rs).

  • basic_authorization(url) follows reqwest step by step:
    • It reads the url crate's username and password and percent-decodes each one.
    • A username that is not UTF-8 sends no field. A password that is not UTF-8 is left out.
    • The value is Basic plus padded standard base64 of {user}: followed by the password, if any.
  • Field order follows the HeaderMap the 1.5.5 request held: the Basic field first, then content-type, then the configured auth_header.
  • Fields are appended the way HeaderMap::append appends them. A configured header also named Authorization is sent as well, right after the Basic field, which is what 1.5.5 sent.
  • Percent-decoding and base64 are written in this crate, so the dependency closure and Cargo.lock do not change.
  • The log masking is unchanged.

Host side. The connector stops refusing the userinfo in a target that comes from the plugin's own settings through target_from, and drops it from the target instead (GetBusbar/busbar#842, which also moves busbar's pin to this head). Until busbar's pin carries that change, the host still refuses such a URL. The sink's code here is correct either way.

REDs (export-webhook/src/tests.rs).

  • a_userinfo_in_the_url_is_sent_as_basic_as_1_5_5_sent_it covers reqwest's decoding and encoding cases: user:pass, user only, empty password, password only, %XX, raw non-ASCII, a stray %, non-UTF-8 password, non-UTF-8 username, no userinfo, and not a URL. It also checks the RFC 4648 vectors.
  • the_userinfo_field_rides_beside_a_configured_auth_header_as_in_1_5_5 checks field order and precedence with Authorization and with another header name. The target stays path plus query.

…, as 1.5.5's reqwest 0.12 sent it

Audit export-webhook #1 (HIGH), ARCHITECT ruling (c). 1.5.5 POSTed through reqwest 0.12, whose
RequestBuilder::new (extract_authority) took the URL's username and password off the URL,
percent-decoded each (a username that is not UTF-8 sends nothing, a password that is not is left
out) and appended basic_auth: 'Basic ' + padded standard base64 of '{user}:' then the password.
The sink now builds that field from its own url setting and sends it first on every POST, then
content-type, then the configured auth header, appended as http::HeaderMap appends: a configured
header also named Authorization is sent too, after the userinfo's, as 1.5.5 sent both. The host's
connector drops the userinfo from the target the url setting gives the need (busbar, same audit
row). Log masking unchanged.

Parity: a_userinfo_in_the_url_is_sent_as_basic_as_1_5_5_sent_it (reqwest's decoding and encoding
cases, RFC 4648 vectors), the_userinfo_field_rides_beside_a_configured_auth_header_as_in_1_5_5.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant