Disclaimer: This is a methodological walkthrough using curl as a case study. The analysis is based on static source code review of curl’s public repository and documented CVE history. This is not original vulnerability research - it’s an educational examination of audit techniques applied to real-world code patterns.
What we’re examining: Connection pooling logic - specifically the architectural patterns that have produced multiple CVEs in curl’s connection reuse matching code.
Why it matters: Connection reuse bugs can leak credentials across origins, bypass TLS requirements, or mix auth states. They’re subtle, high-impact, and represent a recurring vulnerability pattern across many network clients.
The case study: curl’s url_match_conn() function has been patched repeatedly over the years. By analyzing this pattern, we can extract a reusable methodology for auditing similar code in other projects.
The Setup: Understanding Connection Reuse
When you make multiple requests with curl, it doesn’t create a fresh TCP connection every time. It reuses existing connections from a pool. Fast, efficient, but dangerous if the reuse logic gets it wrong.
The core question: “Can this existing connection serve this new request?”
curl answers this in lib/url.c with url_match_conn() - a function that compares ~30 fields between the old connection and the new request:
- Same host? Same port?
- Same user credentials?
- Same TLS requirements?
- Same proxy settings?
- Same auth state?
One missed comparison = CVE.
Phase 1: Connection Reuse Field Gap Analysis
The Hypothesis
Every field on struct connectdata that affects connection behavior must be compared in url_match_conn(). If a field is missed, you get connection reuse where you shouldn’t - wrong TLS level, wrong credentials, wrong protocol state.
The Methodology
This is a static source code analysis approach:
# Step 1: Extract all fields from struct connectdata
# In curl's public repo: lib/urldata.h
grep -A 300 "struct connectdata" lib/urldata.h
# Step 2: Map which fields are checked in url_match_conn()
# In curl's public repo: lib/url.c
grep -n "url_match_" lib/url.c
# Step 3: Build comparison matrix
# Field Name | Where Set | Where Checked | Security Impact
# This is done through source code reading, not dynamic testing
Key technique: Cross-reference data structure fields against the matching logic. Any field that affects connection semantics but isn’t compared is a potential vulnerability class.
Case Study: CVE-2023-27535 Pattern Analysis
Historical CVE: CVE-2023-27535 (discovered by Harry Sintonen, $2400 bounty)
Field: ftp_use_data_ssl (FTP data channel TLS requirement)
Introduced: 2007 (curl 7.13.0)
First compared in matching logic: 2023 (curl 8.0.0)
Exposure window: 15 years
The vulnerability pattern:
// Transfer A: ftp:// with --ftp-ssl-control (control=TLS, data=clear)
// Sets: conn->bits.ftp_use_data_ssl = FALSE
// Transfer B: ftp:// with --ftp-ssl (control=TLS, data=TLS)
// Connection reuse logic checks host, port, user, passwd...
// But DOESN'T check ftp_use_data_ssl
// Transfer B reuses connection from Transfer A
// Reads: conn->bits.ftp_use_data_ssl = FALSE ← STALE VALUE
// Result: Data channel remains CLEARTEXT despite --ftp-ssl request
The fix (commit 8f46084, March 2023):
// Added to ftp_conns_match() in lib/ftp.c
if(nftpc->use_ssl != cftpc->use_ssl)
return FALSE;
This blocks the specific attack by comparing the use_ssl enum. However, the architectural pattern persists: ftp_use_data_ssl still lives on connectdata->bits, still gets set once during connection setup, and is never re-evaluated per-transfer.
Why this matters for methodology: The fix is tactical (add one comparison), not architectural (eliminate the cached state). This pattern will repeat with the next field added to connection state.
Methodology Application: Other Potential Gaps
When applying the field gap analysis method to curl’s source, several other fields warrant investigation:
P0: socks5_gssapi_enctype
Initial concern: GSSAPI encryption type negotiated for SOCKS5 proxy, not compared in matching logic.
Source code review reveals:
// lib/socks.c:1150-1153
if(socksreq[0] != 0) { // enctype != 0
failf(data, "SOCKS5 GSS-API protection not yet implemented.");
return CURLE_COULDNT_CONNECT;
}
Only enctype=0 (no protection) is supported. Other values are rejected at handshake time. Pattern dismissed - no exploitable reuse confusion.
P1: secondaryhostname / secondary_port
Initial concern: FTP data channel host/port not compared in connection matching.
Source code review of FTP data channel lifecycle:
// lib/ftp.c:2205-2207 - Data connection setup
Curl_safefree(ftpc->prevpath);
ftpc->prevpath = NULL;
// Data connection is created fresh per-transfer via PASV/PORT
FTP data channels are ephemeral - created per-transfer, not pooled. Pattern dismissed - no connection reuse of data channels.
P2: ftp_use_data_ssl
Confirmed vulnerability pattern (CVE-2023-27535, now fixed).
Methodological takeaway: Not every uncompared field is exploitable. Static analysis must trace:
- Where the field is set
- Where it’s used
- Whether it persists across the reuse boundary
- Whether existing defenses already mitigate it
The Code That Saved It
Post-CVE-2023-27535, curl added:
// ftp.c:4470 - ftp_conns_match()
if(nftpc->use_ssl != cftpc->use_ssl)
return FALSE;
This blocks the exploit path, but doesn’t fix the root cause. The ftp_use_data_ssl bit still lives on connectdata, still gets set once, still never re-evaluated per-transfer.
Defense-in-depth gap: If any future refactoring weakens this use_ssl check, the vulnerability re-emerges instantly.
Phase 2: Redirect Credential Clearing Audit
The Pattern
curl has had 4 netrc-related CVEs in 15 months. The redirect credential path is clearly fragile. Let’s trace it.
The Hypothesis
When curl follows a redirect to a different origin, it should clear credentials. But what actually gets cleared? And what persists?
The Code Path
// lib/http.c - Curl_http_follow()
if((!same_origin && !data->set.allow_auth_to_other_hosts) ||
!data->set.str[STRING_USERNAME]) {
result = Curl_reset_userpwd(data); // Sets aptr.user from STRING_USERNAME
curlx_safefree(data->state.aptr.user); // Then immediately frees it → NULL
curlx_safefree(data->state.aptr.passwd);
}
What we thought: authhost.done = TRUE might persist and cause auth bypass on the new origin.
What we found: authhost.done is a side-effect flag, not a gate. The real gate is aptr.user.
The Auth Guard
// http.c:795 - Curl_http_output_auth()
if(data->state.aptr.user || authhost->want & NEGOTIATE || STRING_BEARER)
/* continue to output auth headers */;
else {
authhost->done = TRUE; // ← Set as side effect
return CURLE_OK; // ← Skip all auth
}
After cross-origin redirect:
aptr.user = NULL(cleared byCurl_http_follow())- Guard check fails
done = TRUEis set as consequence- No auth headers sent to new origin
This is correct behavior. No credential leak.
The netrc Special Case
But there’s an OR bypass:
// http.c:842
if(Curl_auth_allowed_to_host(data) || conn->bits.netrc)
result = output_auth_headers(...);
Why this is safe:
- After redirect,
aptr.user = NULL override_login()re-reads.netrcfor the NEW hostnameconn->bits.netrc = TRUEonly if new host matched in netrc- Credentials in
aptr.userare for new host, not leaked from old
Verdict: Secure by design. The netrc chain CVEs were hostname comparison bugs (now fixed with Curl_url_same_origin()), not credential persistence bugs.
Phase 3: TLS Backend Feature Parity
The Surface
curl supports 7 TLS backends: OpenSSL, GnuTLS, Schannel, wolfSSL, mbedTLS, rustls, Apple SecureTransport.
Each implements certificate verification, pinning, OCSP, hostname checking independently. Historical CVEs show backends diverge - one backend missing a check = vulnerability.
Check 1: Certificate Pinning (CURLOPT_PINNEDPUBLICKEY)
Initial finding: rustls.c has zero calls to Curl_pin_peer_pubkey(). Pinning appears to be silently ignored.
Reality check:
// setopt.c:2272
if(pinnedpubkey && !(Curl_ssl->supports & SSLSUPP_PINNEDPUBKEY))
result = CURLE_NOT_BUILT_IN;
Enforcement at API boundary. When you set the option on a backend that doesn’t support it, you get CURLE_NOT_BUILT_IN immediately - not a silent failure at connect time.
| Backend | Supports Pinning? | Enforcement |
|---|---|---|
| OpenSSL | ✓ Yes | Runtime check |
| GnuTLS | ✓ Yes | Runtime check |
| Schannel | ✓ Yes | Runtime check |
| wolfSSL | ✓ Conditional | CURLE_NOT_BUILT_IN if no KEEP_PEER_CERT |
| mbedTLS | ✓ Yes | Runtime check |
| rustls | ✗ No | CURLE_NOT_BUILT_IN at setopt |
Check 2: OCSP Stapling (CURLOPT_SSL_VERIFYSTATUS)
Finding: Only OpenSSL and GnuTLS implement OCSP validation. Schannel, wolfSSL, mbedTLS, rustls have zero OCSP code.
But again: API boundary enforcement.
// setopt.c:735
if(!Curl_ssl_cert_status_request())
result = CURLE_NOT_BUILT_IN;
| Backend | Implements OCSP? | Line Count |
|---|---|---|
| OpenSSL | ✓ Yes | ~66 lines |
| GnuTLS | ✓ Yes | ~27 lines |
| Apple | Partial | Delegates to OS |
| Schannel | ✗ No | 0 |
| wolfSSL | ✗ No | 0 |
| mbedTLS | ✗ No | 0 |
| rustls | ✗ No | 0 |
Verdict: No silent bypass. Feature gaps exist, but are declared via capability flags and enforced at setopt time.
The Defense Pattern
curl uses a two-layer approach:
- API layer (setopt): Fail fast if backend doesn’t support feature
- Connect layer: Actual implementation (if supported)
This prevents the “silent security failure” class of bugs entirely.
Phase 4: WebSocket State Machine Audit
The Attack Surface
Protocol state machines are prime targets. The attacker controls server responses, so they can send unexpected state transitions.
WebSocket has:
- Upgrade negotiation (HTTP → WS)
- Frame decoder (header → payload → next frame)
- Fragmentation tracking (TEXT → CONT → CONT+FIN)
What We Checked
// Frame validation checklist:
✓ RSV bits (reserved, must be 0)
✓ Masked frames from server (forbidden per RFC)
✓ Overlong control frames (max 125 bytes)
✓ Control frames during fragmentation (allowed, don't affect continuation)
✓ New data frame mid-fragmentation (error: "interrupted by new TEXT msg")
✓ Continuation without start (error: "no ongoing fragmented message")
✓ Payload length overflow (rejected if > 63 bits)
State Machine Structure
// ws.c - Frame decoder states
enum ws_dec_state {
WS_DEC_INIT, // Ready for new frame
WS_DEC_HEAD, // Reading frame header
WS_DEC_PAYLOAD // Reading payload
};
Transitions are purely data-driven. No hidden assumptions about “we must have done X before Y.” Each state validates its inputs regardless of how it was reached.
One Minor Gap
// ws.c:381 - Fragmentation tracking
if(head_len && (flags & (WSBIT_TEXT | WSBIT_BIN))) {
dec->cont_flags = flags; // ← Only updated for TEXT/BIN, not CLOSE
}
If a CLOSE frame arrives mid-fragmentation, cont_flags retains the CONT bit. Subsequent data frames are still accepted as continuation.
RFC 6455 §5.5.1: After CLOSE, endpoint MUST NOT send more data frames.
curl’s behavior: Decodes whatever the server sends, even after CLOSE.
Security impact: None. The CLOSE is delivered to application first. Processing post-CLOSE data requires application to call curl_ws_recv() again. Server violating the spec just gets its data processed, not exploited.
Verdict: Spec-permissive, not exploitable.
The Architectural Insight: Connection Reuse is Fragile by Design
The Pattern
Every CVE we traced back to connection reuse followed the same structure:
- New field added to
struct connectdata(auth state, TLS setting, protocol option) - Field affects connection behavior
- Developer forgets to add comparison in
url_match_conn() - Years pass
- CVE discovered
- Fix: Add one more comparison
CVE-2023-27535 took 15 years to find. Not because it was hidden, but because there’s no mechanism requiring new fields to be compared.
The Root Cause
struct connectdata {
char *user; // ← Must compare
char *passwd; // ← Must compare
char *sasl_authzid; // ← Must compare
char *oauth_bearer; // ← Must compare
int gssapi_delegation; // ← Must compare
// ... 50+ fields
BIT(ftp_use_data_ssl); // ← Must compare (forgotten for 15 years)
};
bool url_match_conn(needle, check) {
// Manual comparison of ~30 fields
// One forgotten field = CVE
}
Every new field is opt-in for comparison. Opt-in is fragile.
Why Tactical Fixes Don’t Scale
Each CVE gets patched:
// CVE-YYYY-XXXXX: Field X not compared
// Fix: Add comparison
if(needle->X != check->X) return FALSE;
But the architecture that caused the bug remains unchanged. The next field added has the same risk.
Hardening Recommendations
Option A: Explicit Allowlist (Compile-Time Enforced)
Split connectdata into two groups:
struct connectdata {
// Fields that MUST match for reuse
struct {
char *host;
int port;
char *user;
char *passwd;
BIT(ftp_use_data_ssl);
// ... all security-relevant fields
} match_required;
// Fields that don't affect matching
struct {
time_t timestamp;
int refcount;
// ... housekeeping
} meta;
};
Comparison becomes:
bool url_match_conn(needle, check) {
return compare_match_required(&needle->match_required,
&check->match_required);
}
Adding a new field forces the decision: Which struct does it go in?
Effort: ~100 lines (struct split) + ~20 lines (comparison)
Risk: Low (mechanical refactor)
Option B: Per-Field Metadata (Macro-Driven)
#define CONN_FIELD(type, name, flags) type name;
#define MUST_MATCH 0x01
struct connectdata {
CONN_FIELD(char*, user, MUST_MATCH)
CONN_FIELD(char*, passwd, MUST_MATCH)
CONN_FIELD(int, refcount, 0) // Not compared
};
// Comparison auto-generated from flags
Effort: ~150 lines (macro refactor)
Risk: Medium (C preprocessor complexity)
Option C: Runtime Tagging (Conservative)
// When modifying security-relevant field:
conn->user = strdup(user);
conn->needs_fresh = TRUE; // Flag: don't reuse
// In url_match_conn:
if(check->needs_fresh) return FALSE;
Effort: ~30 lines
Risk: Low
Downside: May reject reusable connections (performance hit)
Lessons Learned
1. Historical CVE Patterns Point to Weak Spots
The document we started with listed url_match_conn() as “curl’s most patched function.” That’s not random - it’s a structural weak point. Following that breadcrumb led straight to CVE-2023-27535’s pattern.
2. Defense in Depth Works
curl’s security posture is mature:
- API boundary checks (setopt capability validation)
- Secure defaults (credentials cleared cross-origin)
- Data-driven state machines (no hidden assumptions)
We found no new exploitable vulnerabilities because the codebase has multiple defensive layers.
3. Architectural Debt Persists After CVE Fixes
Tactical fixes stop the immediate bleeding but leave the underlying fragility:
- CVE-2023-27535 fixed by adding
use_sslcomparison - But
ftp_use_data_sslstill cached onconnectdata, never re-evaluated - Next field added has the same risk
Security isn’t just about fixing bugs - it’s about fixing the patterns that cause them.
4. Capability Declaration > Silent Failure
The TLS backend pattern (Phase 3) shows the right approach:
- Backend doesn’t support feature? Declare it via capability flags
- User requests unsupported feature? Fail at setopt with
CURLE_NOT_BUILT_IN - Never silently proceed without the security feature
This eliminates an entire class of “I thought X was protecting me but it wasn’t” bugs.
Methodology Summary
This walkthrough demonstrated four analytical phases applied to curl’s source code:
| Phase | Analytical Technique | Example Finding |
|---|---|---|
| Phase 1 | Field gap analysis (structure vs matching logic) | CVE-2023-27535 pattern: ftp_use_data_ssl unchecked |
| Phase 2 | Control flow tracing (redirect credential clearing) | aptr.user is the real gate, not authhost.done |
| Phase 3 | Multi-implementation comparison (TLS backends) | Feature gaps declared via capability flags at API boundary |
| Phase 4 | State machine validation (WebSocket protocol) | Data-driven transitions, no hidden state assumptions |
Key insight across all phases: curl’s architecture uses defense in depth:
- API boundary checks prevent misconfiguration
- Multiple validation layers for credentials
- Capability declaration prevents silent feature bypass
- State machines validate inputs regardless of path
The one persistent fragility: Connection reuse matching via manual field comparison - a pattern that scales poorly and will continue requiring reactive patches.
Tools & Techniques for Static Analysis
Source Code Navigation
# Field extraction from structures
rg "struct connectdata" lib/urldata.h -A 200
# Function call tracing
rg "url_match_" lib/url.c -n
# Field usage across codebase
rg "ftp_use_data_ssl" lib/ --type c
# Historical context via git
git log --all --grep="CVE-2023-27535"
git blame lib/url.c | grep "use_ssl"
Building the Analysis Matrix
For each field on the connection structure:
- Identification: Field name and type from struct definition
- Setter locations:
rg "field_name\s*=" lib/ --type c - Reader locations:
rg "field_name" lib/ --type c(filter writes) - Matching logic: Is it compared in
url_match_conn()or related functions? - Impact assessment: Does stale value affect security decisions?
Cross-Reference Pattern
# Find all fields in connectdata
grep "struct connectdata" lib/urldata.h -A 300 | \
grep -E "^\s+(char|int|bool|BIT)" | \
awk '{print $NF}' | \
tr -d ';' > /tmp/all_fields.txt
# Find all fields referenced in matching logic
rg "needle->|check->" lib/url.c | \
grep -oP "(?<=->)[a-z_]+" | \
sort -u > /tmp/checked_fields.txt
# Gap analysis
comm -23 /tmp/all_fields.txt /tmp/checked_fields.txt
The Static Analysis Advantage
This entire methodology requires only:
- Source code access (public repository)
- Text search tools (
grep,ripgrep,git) - Understanding of the language (C, in this case)
No compilation, no debugger, no runtime environment needed to identify the architectural vulnerability pattern.
When to Escalate to Dynamic Testing
Static analysis identifies potential vulnerability classes. Dynamic testing confirms:
- Actual exploitability
- Real-world impact
- Edge cases and race conditions
For connection reuse bugs:
- Static analysis: “This field isn’t compared, could cause reuse mismatch”
- Dynamic testing: “Here’s a proof-of-concept showing credential leak to attacker.com”
CVE-2023-27535 was discovered through a combination: static analysis identified the pattern, dynamic testing proved the exploit.
Conclusion: Methodology Over Findings
This walkthrough demonstrates a systematic approach to auditing connection pooling logic in network clients. Using curl as a case study shows:
The Core Technique
- Map data structures - What fields affect connection behavior?
- Trace matching logic - Which fields are compared before reuse?
- Find gaps - Uncompared fields = potential vulnerability class
- Validate impact - Does the gap lead to exploitable behavior?
Why This Matters Beyond curl
The connection reuse pattern appears in:
- HTTP clients: libcurl, requests (Python), httpclient (Java), http.client (Go)
- Database connection pools: HikariCP, pgbouncer, connection pool middleware
- Message queue clients: AMQP, MQTT, Kafka connection managers
- RPC frameworks: gRPC, Thrift connection pooling
The vulnerability class is universal: Cached per-connection state used for per-request decisions creates a reuse mismatch surface.
Architectural Lessons
Fragile pattern: Manual field-by-field comparison
bool can_reuse(old, new) {
if(old->field1 != new->field1) return false;
if(old->field2 != new->field2) return false;
// One forgotten field = vulnerability
}
Robust alternatives:
- Explicit allowlist - Only fields in a designated struct are compared
- Per-field metadata - Declare comparison requirements at field definition
- Stateless design - Don’t cache per-request decisions on per-connection objects
The Value of Static Analysis
This entire analysis was conducted through:
- Source code reading (public curl repository)
- Git history examination (commits, CVE fixes)
- Cross-referencing data structures with usage patterns
No dynamic testing required to identify the vulnerability pattern. The code structure reveals the weakness.
This is the power of systematic static analysis: finding entire vulnerability classes by examining architectural patterns, not just individual bugs.
Applying This Methodology
To audit connection pooling in another project:
- Identify the connection object (
struct connection,Connectionclass, etc.) - Find the reuse matching function (usually named
can_reuse,matches,is_compatible) - Extract all fields from the connection object
- Build the comparison matrix: Field | Security-Relevant? | Compared in Matching?
- For each gap: Trace whether stale values can affect security decisions
- Validate defenses: Are there other layers preventing exploitation?
The pattern that caused CVE-2023-27535 isn’t unique to curl - it’s a fundamental architectural trade-off in any connection pooling system.
References
-
CVE-2023-27535: FTP connection reuse with different TLS data channel requirements
curl - FTP too eager connection reuse - CVE-2023-27535
Discovered by Harry Sintonen -
Key source files for this analysis:
lib/url.c- Connection matching logic (url_match_conn())lib/urldata.h- Connection state structure (struct connectdata)lib/ftp.c- FTP-specific connection handlinglib/http.c- Redirect credential handling
-
Commit 8f46084 (CVE-2023-27535 fix):
Addeduse_ssl,account,alternative_to_user,cccchecks to connection matching
This methodology walkthrough was conducted as a static source code analysis of curl’s public repository (as of early 2025). The techniques demonstrated are applicable to auditing connection pooling logic in any network client. The value is in the systematic approach, not in discovering new vulnerabilities - CVE-2023-27535 was already public and fixed when this analysis was written.
For educational purposes only. When auditing production code, combine static analysis with dynamic testing, fuzzing, and runtime validation.