CVE-2021-22897: schannel cipher selection surprise
What the report claims
The commit added support for selecting ciphers in schannel but used a static algIds array, causing the last configured cipher list to override previous configurations across multiple curl handles, potentially leading to insecure cipher configurations.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2021-22897 Schannel cipher selection surprise
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The code supports the reported flaw. A maintainer should review its impact and decide what to do next.
Why this finding?
- The static ALG_ID algIds[45] array at lib/vtls/schannel.c:335 is shared across all calls to set_ssl_ciphers.
- Each connection's SCHANNEL_CRED receives a pointer to this single static array (line 353).
- When multiple curl handles configure different cipher lists via CURLOPT_SSL_CIPHER_LIST, the last write overwrites the array for all connections.
- This can cause a connection expecting a strong cipher suite to instead use a weaker one configured by another handle, which is a security-relevant misconfiguration.
- The bug exists in both the master revision (e540b32562a0) and the release curl-7_76_1 (same static array at line 331).
How this analysis ran 5 claims · 10 citations verified
Checked against curl master on 2021-04-22 (e540b32562a0). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Review a draft reply to the reporter
Edit and verify before sending.
Thank you for the report. The static `algIds` array in `set_ssl_ciphers` (lib/vtls/schannel.c:335) is indeed shared across all connections using the schannel backend. Each connection's credential points to this single array (line 353), so the last handle to set CURLOPT_SSL_CIPHER_LIST overwrites the cipher list for all concurrent connections. This can cause a connection to use a weaker cipher suite than configured. We will fix this by making the cipher list per-connection (e.g., storing it in SCHANNEL_CRED).
Inspect all claims and source code 5 claims · 4 code excerpts
Report claims
The static `algIds` array in `set_ssl_ciphers` causes the last configured cipher list to override configuration used by other connections
Read the reasoning
The static array is global to the function. Every call to set_ssl_ciphers writes into the same array, and each connection's SCHANNEL_CRED ends up pointing to that single array. The last caller's cipher list overwrites the array for all connections.
in `set_ssl_ciphers`
Read the reasoning
The flaw is located in the set_ssl_ciphers function as shown.
using multiple curl handles with `CURLOPT_SSL_CIPHER_LIST` set to different cipher lists
Read the reasoning
The code path exists: multiple easy handles can be created and each can set a different cipher list via CURLOPT_SSL_CIPHER_LIST, which will invoke set_ssl_ciphers for the schannel backend.
potential wrong configuration for connections
Read the reasoning
Because all connections share the static array, a connection may use a cipher list configured by a different handle, leading to wrong (potentially weaker) cipher configuration.
Create two or more separate curl handles with `curl_easy_init`, set different cipher lists with `CURLOPT_SSL_CIPHER_LIST`, and create simultaneous connections
Read the reasoning
The reproduction steps map directly to the code: create multiple handles, set different cipher lists on each, and initiate connections. The static array bug will cause the last configuration to be seen by all.
Read the original report
Source code
| 330 | static CURLcode |
| C2: function definition: static CURLcode set_ssl_ciphers(SCHANNEL_CRED *schannel_cred, char *ciphers) | |
| C5: set_ssl_ciphers processes the cipher list | |
| 331 | set_ssl_ciphers(SCHANNEL_CRED *schannel_cred, char *ciphers) |
| 332 | { |
| 333 | char *startCur = ciphers; |
| 334 | int algCount = 0; |
| 335 | static ALG_ID algIds[45]; /*There are 45 listed in the MS headers*/ |
| C1: static ALG_ID algIds[45]; declares a single static array shared by all calls | |
| C4: static array shared across all connections | |
| 336 | while(startCur && (0 != *startCur) && (algCount < 45)) { |
| 337 | long alg = strtol(startCur, 0, 0); |
| 338 | if(!alg) |
| 339 | alg = get_alg_id_by_name(startCur); |
| 340 | if(alg) |
| 341 | algIds[algCount++] = alg; |
| 342 | else if(!strncmp(startCur, "USE_STRONG_CRYPTO", |
| 343 | sizeof("USE_STRONG_CRYPTO") - 1) || |
| 344 | !strncmp(startCur, "SCH_USE_STRONG_CRYPTO", |
| 345 | sizeof("SCH_USE_STRONG_CRYPTO") - 1)) |
| 346 | schannel_cred->dwFlags |= SCH_USE_STRONG_CRYPTO; |
| 347 | else |
| 348 | return CURLE_SSL_CIPHER; |
| 349 | startCur = strchr(startCur, ':'); |
| 350 | if(startCur) |
| 351 | startCur++; |
| 352 | } |
| 353 | schannel_cred->palgSupportedAlgs = algIds; |
| C1: schannel_cred->palgSupportedAlgs = algIds; assigns the static array's address to each connection's credential | |
| C4: each connection's credential points to the same array | |
| 354 | schannel_cred->cSupportedAlgs = algCount; |
| 355 | return CURLE_OK; |
| 356 | } |
| 289 | struct Curl_easy *curl_easy_init(void) |
| C3: curl_easy_init creates separate handles | |
| C5: curl_easy_init creates handles | |
| 290 | { |
| 291 | CURLcode result; |
| 292 | struct Curl_easy *data; |
| 293 | |
| 294 | /* Make sure we inited the global SSL stuff */ |
| 295 | if(!initialized) { |
| 296 | result = curl_global_init(CURL_GLOBAL_DEFAULT); |
| 297 | if(result) { |
| 298 | /* something in the global init failed, return nothing */ |
| 299 | DEBUGF(fprintf(stderr, "Error: curl_global_init failed\n")); |
| 300 | return NULL; |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | /* We use curl_open() with undefined URL so far */ |
| 305 | result = Curl_open(&data); |
| 306 | if(result) { |
| 307 | DEBUGF(fprintf(stderr, "Error: Curl_open failed\n")); |
| 308 | return NULL; |
| 309 | } |
| 310 | |
| 311 | return data; |
| 312 | } |
| 3000 | CURLcode curl_easy_setopt(struct Curl_easy *data, CURLoption tag, ...) |
| C3: curl_easy_setopt handles CURLOPT_SSL_CIPHER_LIST which would call into schannel's set_ssl_ciphers | |
| C5: curl_easy_setopt sets options including CURLOPT_SSL_CIPHER_LIST | |
| 3001 | { |
| 3002 | va_list arg; |
| 3003 | CURLcode result; |
| 3004 | |
| 3005 | if(!data) |
| 3006 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 3007 | |
| 3008 | va_start(arg, tag); |
| 3009 | |
| 3010 | result = Curl_vsetopt(data, tag, arg); |
| 3011 | |
| 3012 | va_end(arg); |
| 3013 | return result; |
| 3014 | } |
Show 22 lines (not cited by the judge)
| 326 | static CURLcode |
| 327 | set_ssl_ciphers(SCHANNEL_CRED *schannel_cred, char *ciphers) |
| 328 | { |
| 329 | char *startCur = ciphers; |
| 330 | int algCount = 0; |
| 331 | static ALG_ID algIds[45]; /*There are 45 listed in the MS headers*/ |
| 332 | while(startCur && (0 != *startCur) && (algCount < 45)) { |
| 333 | long alg = strtol(startCur, 0, 0); |
| 334 | if(!alg) |
| 335 | alg = get_alg_id_by_name(startCur); |
| 336 | if(alg) |
| 337 | algIds[algCount++] = alg; |
| 338 | else |
| 339 | return CURLE_SSL_CIPHER; |
| 340 | startCur = strchr(startCur, ':'); |
| 341 | if(startCur) |
| 342 | startCur++; |
| 343 | } |
| 344 | schannel_cred->palgSupportedAlgs = algIds; |
| 345 | schannel_cred->cSupportedAlgs = algCount; |
| 346 | return CURLE_OK; |
| 347 | } |