Curl Alt-Svc Parser Stack Buffer Overflow
What the report claims
A critical stack buffer overflow in cURL's Alt-Svc header parser allows remote code execution via oversized Alt-Svc headers.
Compare with curl's recorded outcome
Labeled AI slop
curl's maintainer lists this report as AI slop
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The code provides evidence against the report's central claim. Review the cited lines before deciding.
Why this finding?
- The report's central claim about buffer size (101 bytes / MAX_ALTSVC_DATELEN=100) is contradicted by the provided source code which shows MAX_ALTSVC_DATELEN=256 at lib/altsvc.c:46
- The actual Alt-Svc header parsing code that would contain the alleged memcpy is not included in the provided source snippets, so the claimed flaw cannot be verified
- The deterministic checks show false positives (e.g., Python's struct.pack reported as a curl symbol), reducing confidence in their grounding
- The report targets versions 7.64.0-7.89.0 but the checked revision is master at curl-8_0_0 where MAX_ALTSVC_DATELEN=256; the report's buffer size claim doesn't match the provided code at any version shown
What still needs checking
- The Alt-Svc parsing function (likely in lib/altsvc.c) showing the memcpy and any bounds checks
- Confirmation of MAX_ALTSVC_DATELEN value in the specific vulnerable versions (7.64.0-7.89.0)
How this analysis ran 6 claims · 1 citations verified
Checked against curl master on 2025-12-16 (7b2783496ce7). 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.
The report claims a 101-byte stack buffer (MAX_ALTSVC_DATELEN=100), but the provided source shows MAX_ALTSVC_DATELEN=256 at lib/altsvc.c:46, making the buffer 257 bytes. The actual Alt-Svc parsing code with the alleged memcpy is not in the provided snippets. Please provide the specific parsing function and confirm the MAX_ALTSVC_DATELEN value in the versions you tested (7.64.0-7.89.0). Without the vulnerable code path, the overflow claim cannot be verified.
Inspect all claims and source code 6 claims · 6 code excerpts
Report claims
The code creates a fixed-size buffer on the stack: char dbuf[MAX_ALTSVC_DATELEN + 1] (101 bytes)
Read the reasoning
The report claims MAX_ALTSVC_DATELEN + 1 = 101 bytes, implying MAX_ALTSVC_DATELEN = 100. The provided source shows MAX_ALTSVC_DATELEN = 256 at lib/altsvc.c:46.
It blindly copies user-controlled data into this buffer: memcpy(dbuf, Curl_dyn_ptr(&date), Curl_dyn_len(&date))
Read the reasoning
The report references memcpy with Curl_dyn_ptr and Curl_dyn_len, but the actual Alt-Svc parsing code containing this memcpy is not in the provided source snippets. The deterministic checks only confirm these symbols exist, not how they're used.
There's no check to verify if the input data exceeds the buffer size
Read the reasoning
The claim about a missing bounds check depends on the parsing code which is not provided. Cannot verify from given source.
An attacker can provide a date string much larger than 101 bytes, overwriting adjacent memory
Read the reasoning
Depends on the parsing code and actual buffer size. The buffer size claim (101 bytes) is contradicted by the provided code (257 bytes). The parsing code is not shown.
The vulnerability is a classic stack buffer overflow (CWE-121)
Read the reasoning
Classification as CWE-121 depends on the actual flaw existing. The core buffer size claim is contradicted and the vulnerable code path is not in the provided source.
It can lead to complete system compromise
Read the reasoning
Impact claim without code references. Depends on the vulnerability being real and exploitable, which is not established by the provided code.
Read the original report (excerpt)
Source code
| 46 | #define MAX_ALTSVC_DATELEN 256 |
| C1: MAX_ALTSVC_DATELEN is defined as 256, not 100. The buffer would be 257 bytes, not 101 bytes as claimed. |
| 276 | # define CURL_DISABLE_ALTSVC 1 |
Show 12 lines (not cited by the judge)
| 248 | int main(int argc, char **argv) |
| 249 | { |
| 250 | size_t i; |
| 251 | |
| 252 | (void)argc; |
| 253 | (void)argv; |
| 254 | |
| 255 | for(i = 0; i < CURL_ARRAYSIZE(disabled); i++) |
| 256 | puts(disabled[i]); |
| 257 | |
| 258 | return 0; |
| 259 | } |
Show 13 lines (not cited by the judge)
| 241 | int main(int argc, char **argv) |
| 242 | { |
| 243 | size_t i; |
| 244 | |
| 245 | (void)argc; |
| 246 | (void)argv; |
| 247 | |
| 248 | for(i = 0; i < CURL_ARRAYSIZE(disabled); i++) |
| 249 | /* !checksrc! disable BANNEDFUNC 1 */ |
| 250 | printf("%s\n", disabled[i]); |
| 251 | |
| 252 | return 0; |
| 253 | } |
Show 97 lines (not cited by the judge)
| 3024 | ParameterError parse_args(int argc, argv_item_t argv[]) |
| 3025 | { |
| 3026 | int i; |
| 3027 | bool stillflags; |
| 3028 | const char *orig_opt = NULL; |
| 3029 | ParameterError result = PARAM_OK; |
| 3030 | struct OperationConfig *config = global->first; |
| 3031 | |
| 3032 | for(i = 1, stillflags = TRUE; i < argc && !result; i++) { |
| 3033 | orig_opt = convert_tchar_to_UTF8(argv[i]); |
| 3034 | if(!orig_opt) |
| 3035 | return PARAM_NO_MEM; |
| 3036 | |
| 3037 | if(stillflags && ('-' == orig_opt[0])) { |
| 3038 | bool passarg; |
| 3039 | |
| 3040 | if(!strcmp("--", orig_opt)) |
| 3041 | /* This indicates the end of the flags and thus enables the |
| 3042 | following (URL) argument to start with -. */ |
| 3043 | stillflags = FALSE; |
| 3044 | else { |
| 3045 | const char *nextarg = NULL; |
| 3046 | if(i < (argc - 1)) { |
| 3047 | nextarg = convert_tchar_to_UTF8(argv[i + 1]); |
| 3048 | if(!nextarg) { |
| 3049 | unicodefree(CURL_UNCONST(orig_opt)); |
| 3050 | return PARAM_NO_MEM; |
| 3051 | } |
| 3052 | } |
| 3053 | |
| 3054 | result = getparameter(orig_opt, nextarg, &passarg, config, |
| 3055 | CONFIG_MAX_LEVELS); |
| 3056 | |
| 3057 | unicodefree(CURL_UNCONST(nextarg)); |
| 3058 | config = global->last; |
| 3059 | if(result == PARAM_NEXT_OPERATION) { |
| 3060 | /* Reset result as PARAM_NEXT_OPERATION is only used here and not |
| 3061 | returned from this function */ |
| 3062 | result = PARAM_OK; |
| 3063 | |
| 3064 | if(config->url_list && config->url_list->url) { |
| 3065 | /* Allocate the next config */ |
| 3066 | config->next = config_alloc(); |
| 3067 | if(config->next) { |
| 3068 | /* Update the last config pointer */ |
| 3069 | global->last = config->next; |
| 3070 | |
| 3071 | /* Move onto the new config */ |
| 3072 | config->next->prev = config; |
| 3073 | config = config->next; |
| 3074 | } |
| 3075 | else |
| 3076 | result = PARAM_NO_MEM; |
| 3077 | } |
| 3078 | else { |
| 3079 | errorf("missing URL before --next"); |
| 3080 | result = PARAM_BAD_USE; |
| 3081 | } |
| 3082 | } |
| 3083 | else if(!result && passarg) |
| 3084 | i++; /* we are supposed to skip this */ |
| 3085 | } |
| 3086 | } |
| 3087 | else { |
| 3088 | bool used; |
| 3089 | |
| 3090 | /* Just add the URL please */ |
| 3091 | result = getparameter("--url", orig_opt, &used, config, 0); |
| 3092 | } |
| 3093 | |
| 3094 | if(!result) { |
| 3095 | unicodefree(CURL_UNCONST(orig_opt)); |
| 3096 | orig_opt = NULL; |
| 3097 | } |
| 3098 | } |
| 3099 | |
| 3100 | if(!result && config->content_disposition) { |
| 3101 | if(config->resume_from_current) |
| 3102 | result = PARAM_CONTDISP_RESUME_FROM; |
| 3103 | } |
| 3104 | |
| 3105 | if(result && result != PARAM_HELP_REQUESTED && |
| 3106 | result != PARAM_MANUAL_REQUESTED && |
| 3107 | result != PARAM_VERSION_INFO_REQUESTED && |
| 3108 | result != PARAM_ENGINES_REQUESTED && |
| 3109 | result != PARAM_CA_EMBED_REQUESTED) { |
| 3110 | const char *reason = param2text(result); |
| 3111 | |
| 3112 | if(orig_opt && strcmp(":", orig_opt)) |
| 3113 | helpf("option %s: %s", orig_opt, reason); |
| 3114 | else |
| 3115 | helpf("%s", reason); |
| 3116 | } |
| 3117 | |
| 3118 | unicodefree(CURL_UNCONST(orig_opt)); |
| 3119 | return result; |
| 3120 | } |
Show 97 lines (not cited by the judge)
| 2999 | ParameterError parse_args(int argc, argv_item_t argv[]) |
| 3000 | { |
| 3001 | int i; |
| 3002 | bool stillflags; |
| 3003 | const char *orig_opt = NULL; |
| 3004 | ParameterError result = PARAM_OK; |
| 3005 | struct OperationConfig *config = global->first; |
| 3006 | |
| 3007 | for(i = 1, stillflags = TRUE; i < argc && !result; i++) { |
| 3008 | orig_opt = convert_tchar_to_UTF8(argv[i]); |
| 3009 | if(!orig_opt) |
| 3010 | return PARAM_NO_MEM; |
| 3011 | |
| 3012 | if(stillflags && ('-' == orig_opt[0])) { |
| 3013 | bool passarg; |
| 3014 | |
| 3015 | if(!strcmp("--", orig_opt)) |
| 3016 | /* This indicates the end of the flags and thus enables the |
| 3017 | following (URL) argument to start with -. */ |
| 3018 | stillflags = FALSE; |
| 3019 | else { |
| 3020 | const char *nextarg = NULL; |
| 3021 | if(i < (argc - 1)) { |
| 3022 | nextarg = convert_tchar_to_UTF8(argv[i + 1]); |
| 3023 | if(!nextarg) { |
| 3024 | unicodefree(orig_opt); |
| 3025 | return PARAM_NO_MEM; |
| 3026 | } |
| 3027 | } |
| 3028 | |
| 3029 | result = getparameter(orig_opt, nextarg, &passarg, config, |
| 3030 | CONFIG_MAX_LEVELS); |
| 3031 | |
| 3032 | unicodefree(nextarg); |
| 3033 | config = global->last; |
| 3034 | if(result == PARAM_NEXT_OPERATION) { |
| 3035 | /* Reset result as PARAM_NEXT_OPERATION is only used here and not |
| 3036 | returned from this function */ |
| 3037 | result = PARAM_OK; |
| 3038 | |
| 3039 | if(config->url_list && config->url_list->url) { |
| 3040 | /* Allocate the next config */ |
| 3041 | config->next = config_alloc(); |
| 3042 | if(config->next) { |
| 3043 | /* Update the last config pointer */ |
| 3044 | global->last = config->next; |
| 3045 | |
| 3046 | /* Move onto the new config */ |
| 3047 | config->next->prev = config; |
| 3048 | config = config->next; |
| 3049 | } |
| 3050 | else |
| 3051 | result = PARAM_NO_MEM; |
| 3052 | } |
| 3053 | else { |
| 3054 | errorf("missing URL before --next"); |
| 3055 | result = PARAM_BAD_USE; |
| 3056 | } |
| 3057 | } |
| 3058 | else if(!result && passarg) |
| 3059 | i++; /* we are supposed to skip this */ |
| 3060 | } |
| 3061 | } |
| 3062 | else { |
| 3063 | bool used; |
| 3064 | |
| 3065 | /* Just add the URL please */ |
| 3066 | result = getparameter("--url", orig_opt, &used, config, 0); |
| 3067 | } |
| 3068 | |
| 3069 | if(!result) { |
| 3070 | unicodefree(orig_opt); |
| 3071 | orig_opt = NULL; |
| 3072 | } |
| 3073 | } |
| 3074 | |
| 3075 | if(!result && config->content_disposition) { |
| 3076 | if(config->resume_from_current) |
| 3077 | result = PARAM_CONTDISP_RESUME_FROM; |
| 3078 | } |
| 3079 | |
| 3080 | if(result && result != PARAM_HELP_REQUESTED && |
| 3081 | result != PARAM_MANUAL_REQUESTED && |
| 3082 | result != PARAM_VERSION_INFO_REQUESTED && |
| 3083 | result != PARAM_ENGINES_REQUESTED && |
| 3084 | result != PARAM_CA_EMBED_REQUESTED) { |
| 3085 | const char *reason = param2text(result); |
| 3086 | |
| 3087 | if(orig_opt && strcmp(":", orig_opt)) |
| 3088 | helpf("option %s: %s", orig_opt, reason); |
| 3089 | else |
| 3090 | helpf("%s", reason); |
| 3091 | } |
| 3092 | |
| 3093 | unicodefree(orig_opt); |
| 3094 | return result; |
| 3095 | } |