✓ProveIt
Examples / Report #3403880 Saved analysis

Logical Flaw in curl_url_set Leads to Inconsistent Query Parameter Encoding

curl · 2025-10-2938.0s analysis · $0.032 in model calls

What the report claims

curl_url_set() exhibits inconsistent URL encoding behavior for CURLUPART_QUERY based on CURLU_APPENDQUERY flag rather than CURLU_URLENCODE, causing HTTP Parameter Pollution vulnerabilities.

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

Needs a policy review

The code does what the report says, but curl's own policy may exclude it. A maintainer decides.

curl policy: API misuse
Judgment withdrawn. The answer failed the evidence check.
See the withdrawn model reasoning

The following reasoning failed the citation check and does not support the final finding.

  • The divergent '=' encoding is intentional: CURLU_APPENDQUERY is documented as 'append a form style part' (include/curl/urlapi.h:95), meaning the input is a single key=value pair where the first '=' is a delimiter. The replace path treats the input as a raw string. This is a documented semantic difference, not a bug.
  • The claimed HTTP Parameter Pollution (HPP) does not occur because all other special characters (:, /, &, etc.) are still percent-encoded in both modes (lib/urlapi.c:1866-1881). The attack scenario's example 'callback=http://attacker.com' would have ':' and '/' encoded to '%3A' and '%2F' even in append mode.
  • The project's 'API misuse' policy applies: the report describes a behavior difference between two documented API modes, not a flaw that gives an attacker capabilities they did not already have. Applications choose the mode appropriate to their input format.
How this analysis ran 5 claims · 11 citations verified

Evidence check: the behavior is real; the project's policy section 'API misuse' may exclude it

Checked against curl master on 2025-10-29 (a97f9d41cc01). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
grounded
no model · 7 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 3916 tokens · $0.0003
3 · Evidence
2 code snippets
real curl source at a97f9d41cc01 · 5 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
18251 tokens · 35.0s · $0.0317
5 · Receipts
11 of 11 citations verified
every citation must point at code the judge was shown
Inspect the withdrawn draft reply

This draft belongs to the withdrawn judgment. It may contain unsupported claims.

Thank you for the detailed report and PoC. The code shows the different '=' handling is intentional: CURLU_APPENDQUERY means 'append a form-style key=value part' (see include/curl/urlapi.h:95), so the first '=' is a delimiter and is preserved; the replace path treats the input as a raw string. This semantic difference is documented by the flag's purpose. Crucially, all other special characters (:, /, &, etc.) are still percent-encoded in both modes (lib/urlapi.c:1866-1881), so the described HPP vector (injecting unencoded ':' or '/') does not occur. The PoC confirms the behavioral difference, but it is a documented API distinction, not a security flaw. We consider this 'API misuse' per our policy — the application must choose the mode matching its input format.

Inspect all claims and source code 5 claims · 2 code excerpts

This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.

Report claims

C1flaw supported

The logic that determines whether to percent-encode the = character is incorrectly tied to the CURLU_APPENDQUERY flag, rather than being based purely on the CURLU_URLENCODE flag.

Read the reasoning

The code confirms that equalsencode is set to appendquery (line 1832), which is derived from CURLU_APPENDQUERY (line 1831), not from urlencode (CURLU_URLENCODE). This is the logical coupling the report describes.

C2location supported

Line ~1832 in lib/urlapi.c where equalsencode = appendquery;

Read the reasoning

The cited line exists and matches the report's description.

C3trigger supported

The inconsistency manifests when the same input string is processed with CURLU_URLENCODE flag during replace vs. append operations.

Read the reasoning

The code path shows that when CURLU_URLENCODE is set without CURLU_APPENDQUERY, equalsencode is FALSE and '=' is encoded; when both flags are set, equalsencode is TRUE and the first '=' is not encoded. The PoC output confirms this divergence.

C4impact contradicted

This can lead to HTTP Parameter Pollution (HPP) vulnerabilities in applications that rely on libcurl.

Read the reasoning

The attack scenario claims unencoded ':', '/', '&' can be injected. However, the encoding loop (lines 1856-1882) percent-encodes every character that is not ISUNRESERVED, not allowed_in_path, and not the first '=' when equalsencode is TRUE. Colon, slash, and ampersand are not in the unreserved set and are therefore encoded in both replace and append modes. The PoC shows '&' becomes '%26' in both Test 2 and Test 3. The claimed HPP vector does not materialize.

C5reproduction supported

The PoC demonstrates that the exact same input string ("a=b&c=d") is encoded differently depending on whether CURLU_APPENDQUERY is present.

Read the reasoning

The PoC demonstrates the exact same input 'a=b&c=d' produces 'a%3db%26c%3dd' (replace+encode) vs 'a=b%26c%3dd' (append+encode). The code confirms this is the current behavior.

What the report says about other projects

not addressed The logic for equalsencode in lib/urlapi.c (around line 1832) should not be tied to appendquery. The decision to encode = in a query string should be based only on whether CURLU_URLENCODE is set.

Read the original report (excerpt)

Full report on HackerOne ↗

Hello curl security team, First, thank you for your incredible work on maintaining such a critical and robust piece of software. We have been conducting a deep-dive source code audit of libcurl and believe we have identified a subtle logical flaw in the curl_url_set API that has security implications. ## Summary The curl_url_set() function in lib/urlapi.c exhibits inconsistent URL encoding behavior for the CURLUPART_QUERY part. The logic that determines whether to percent-encode the = character is incorrectly tied to the CURLU_APPENDQUERY flag, rather than being based purely on the CURLU_URLENCODE flag. This causes the exact same input string to be encoded in two different ways depending on whether the query is being replaced or appended, which can lead to HTTP Parameter Pollution (HPP) vulnerabilities in applications that rely on libcurl. ## Affected Code lib/urlapi.c, function curl_url_set(), specifically: 1. Line ~1832: equalsencode = appendquery; * This logic incorrectly bases the equalsencode flag on whether an append operation is happening. 2. Line ~1868: ((*i == '=') && equalsencode) * This line later uses equalsencode to decide whether to skip encoding the = character. Because equalsencode is only true during an append operation, a replace operation (even with CURLU_URLENCODE) will result in equalsencode being false, causing it to incorrectly encode the = character. ## Proof of Concept (PoC) In the interest of full transparency, an AI assistant was utilized to help write and refine the C code for the accompanying Proof of Concept. To confirm this inconsistent behavior, we wrote the following C program. It tests the exact same input string ("a=b&c=d") under three different scenarios using the curl_url_set API. PoC Code (poc.c): ``` /* * =================================================================== * PoC: Inconsistent URL Query Encoding in curl_url_set (v3.1 Final) * =================================================================== * * - Target: libcurl / lib/urlapi.c / curl_url_set() * - Vulnerability: CWE-20: Improper Input Validation * (leading to CWE-436: Interpretation Conflict) * * - Analysis: * We discovered a logical flaw in how curl_url_set() handles URL * encoding for the CURLUPART_QUERY part. The logic that skips * encoding the '=' character (`equalsencode`) is only activated * if the `CURLU_APPEND …

Source code

lib/urlapi.clines 1774–1934 · lines 1832-1832 cited by the report; definition of curl_url_set (truncated)
1774CURLUcode curl_url_set(CURLU *u, CURLUPart what,
1775 const char *part, unsigned int flags)
1776{
1777 char **storep = NULL;
1778 bool urlencode = (flags & CURLU_URLENCODE) ? 1 : 0;
C1: urlencode is derived from CURLU_URLENCODE
1779 bool plusencode = FALSE;
1780 bool pathmode = FALSE;
1781 bool leadingslash = FALSE;
1782 bool appendquery = FALSE;
1783 bool equalsencode = FALSE;
1784 size_t nalloc;
1785
1786 if(!u)
1787 return CURLUE_BAD_HANDLE;
1788 if(!part)
1789 /* setting a part to NULL clears it */
1790 return urlset_clear(u, what);
1791
1792 nalloc = strlen(part);
1793 if(nalloc > CURL_MAX_INPUT_LENGTH)
1794 /* excessive input length */
1795 return CURLUE_MALFORMED_INPUT;
1796
1797 switch(what) {
1798 case CURLUPART_SCHEME: {
1799 CURLUcode status = set_url_scheme(u, part, flags);
1800 if(status)
1801 return status;
1802 storep = &u->scheme;
1803 urlencode = FALSE; /* never */
1804 break;
1805 }
1806 case CURLUPART_USER:
1807 storep = &u->user;
1808 break;
1809 case CURLUPART_PASSWORD:
1810 storep = &u->password;
1811 break;
1812 case CURLUPART_OPTIONS:
1813 storep = &u->options;
1814 break;
1815 case CURLUPART_HOST:
1816 storep = &u->host;
1817 Curl_safefree(u->zoneid);
1818 break;
1819 case CURLUPART_ZONEID:
1820 storep = &u->zoneid;
1821 break;
1822 case CURLUPART_PORT:
1823 return set_url_port(u, part);
1824 case CURLUPART_PATH:
1825 pathmode = TRUE;
1826 leadingslash = TRUE; /* enforce */
1827 storep = &u->path;
1828 break;
1829 case CURLUPART_QUERY:
1830 plusencode = urlencode;
1831 appendquery = (flags & CURLU_APPENDQUERY) ? 1 : 0;
C3: appendquery = (flags & CURLU_APPENDQUERY) ? 1 : 0;
1832 equalsencode = appendquery;
C1: equalsencode = appendquery; ties the flag to CURLU_APPENDQUERY
C2: equalsencode = appendquery; at line 1832 in the CURLUPART_QUERY case
C3: equalsencode = appendquery;
C5: equalsencode = appendquery; causes the divergence
1833 storep = &u->query;
1834 u->query_present = TRUE;
1835 break;
1836 case CURLUPART_FRAGMENT:
1837 storep = &u->fragment;
1838 u->fragment_present = TRUE;
1839 break;
1840 case CURLUPART_URL:
1841 return set_url(u, part, nalloc, flags);
1842 default:
1843 return CURLUE_UNKNOWN_PART;
1844 }
1845 DEBUGASSERT(storep);
1846 {
1847 const char *newp;
1848 struct dynbuf enc;
1849 curlx_dyn_init(&enc, nalloc * 3 + 1 + leadingslash);
1850
1851 if(leadingslash && (part[0] != '/')) {
1852 CURLcode result = curlx_dyn_addn(&enc, "/", 1);
1853 if(result)
1854 return cc2cu(result);
1855 }
1856 if(urlencode) {
1857 const unsigned char *i;
1858
1859 for(i = (const unsigned char *)part; *i; i++) {
1860 CURLcode result;
1861 if((*i == ' ') && plusencode) {
1862 result = curlx_dyn_addn(&enc, "+", 1);
1863 if(result)
1864 return CURLUE_OUT_OF_MEMORY;
1865 }
1866 else if(ISUNRESERVED(*i) ||
C4: ISUNRESERVED(*i) and allowed_in_path(*i) guard other characters
1867 (pathmode && allowed_in_path(*i)) ||
1868 ((*i == '=') && equalsencode)) {
C1: ((*i == '=') && equalsencode) uses equalsencode to skip encoding '='
C3: ((*i == '=') && equalsencode) controls '=' encoding
C5: First '=' skipped only when equalsencode is TRUE
1869 if((*i == '=') && equalsencode)
1870 /* only skip the first equals sign */
1871 equalsencode = FALSE;
1872 result = curlx_dyn_addn(&enc, i, 1);
1873 if(result)
1874 return cc2cu(result);
1875 }
1876 else {
1877 unsigned char out[3]={'%'};
C4: Non-unreserved characters (:, /, &, etc.) are percent-encoded via curlx_dyn_addn
1878 Curl_hexbyte(&out[1], *i);
1879 result = curlx_dyn_addn(&enc, out, 3);
1880 if(result)
1881 return cc2cu(result);
1882 }
1883 }
1884 }
1885 else {
1886 char *p;
1887 CURLcode result = curlx_dyn_add(&enc, part);
1888 if(result)
1889 return cc2cu(result);
1890 p = curlx_dyn_ptr(&enc);
1891 while(*p) {
1892 /* make sure percent encoded are lower case */
1893 if((*p == '%') && ISXDIGIT(p[1]) && ISXDIGIT(p[2]) &&
1894 (ISUPPER(p[1]) || ISUPPER(p[2]))) {
1895 p[1] = Curl_raw_tolower(p[1]);
1896 p[2] = Curl_raw_tolower(p[2]);
1897 p += 3;
1898 }
1899 else
1900 p++;
1901 }
1902 }
1903 newp = curlx_dyn_ptr(&enc);
1904
1905 if(appendquery && newp) {
1906 /* Append the 'newp' string onto the old query. Add a '&' separator if
1907 none is present at the end of the existing query already */
1908
1909 size_t querylen = u->query ? strlen(u->query) : 0;
1910 bool addamperand = querylen && (u->query[querylen -1] != '&');
1911 if(querylen) {
1912 struct dynbuf qbuf;
1913 curlx_dyn_init(&qbuf, CURL_MAX_INPUT_LENGTH);
1914
1915 if(curlx_dyn_addn(&qbuf, u->query, querylen)) /* add original query */
1916 goto nomem;
1917
1918 if(addamperand) {
1919 if(curlx_dyn_addn(&qbuf, "&", 1))
1920 goto nomem;
1921 }
1922 if(curlx_dyn_add(&qbuf, newp))
1923 goto nomem;
1924 curlx_dyn_free(&enc);
1925 free(*storep);
1926 *storep = curlx_dyn_ptr(&qbuf);
1927 return CURLUE_OK;
1928nomem:
1929 curlx_dyn_free(&enc);
1930 return CURLUE_OUT_OF_MEMORY;
1931 }
1932 }
1933
1934 else if(what == CURLUPART_HOST) {
include/curl/urlapi.hlines 94–95 · definition of CURLU_APPENDQUERY; definition of CURLU_URLENCODE
94#define CURLU_URLENCODE (1<<7) /* URL encode on set */
95#define CURLU_APPENDQUERY (1<<8) /* append a form style part */