From dd4f40ad5ef173dacd751b0ee14484cbe1975f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 18:51:29 +0200 Subject: [PATCH 1/5] Adding max reponse size for HTTP2 --- lib/core/settings.py | 2 +- lib/request/http2.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 1cb6c5ddf5..0a35a7fce8 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.46" +VERSION = "1.10.7.47" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/request/http2.py b/lib/request/http2.py index 17c768068c..a8aa6287f7 100644 --- a/lib/request/http2.py +++ b/lib/request/http2.py @@ -377,6 +377,12 @@ def encode(self, headers): SETTINGS_INITIAL_WINDOW_SIZE = 0x4 BIG_WINDOW = (1 << 31) - 1 +# Upper bound on the response bytes (body or header block) buffered per stream. The client advertises a +# ~2GB flow-control window, so without this a large (or hostile) server would drive the whole body into +# memory and OOM the process. Mirrors the HTTP/1.1 path's MAX_CONNECTION_TOTAL_SIZE (100MB) cap in +# connect.py; a stream that exceeds it is truncated (body) or abandoned (headers) and its connection retired. +MAX_RESPONSE_SIZE = 100 * 1024 * 1024 + def _recv_exact(sock, n): buf = b"" while len(buf) < n: @@ -524,6 +530,9 @@ def exchange(self, method, path, authority, headers, body, timeout): if flags & FLAG_PRIORITY: p = p[5:] header_block += p + if len(header_block) > MAX_RESPONSE_SIZE: # hostile/endless header block -> bail rather than OOM + self.usable = False + raise IOError("oversized HTTP/2 header block") if flags & FLAG_END_HEADERS: resp_headers = self.dec.decode(header_block) if flags & FLAG_END_STREAM: @@ -533,6 +542,9 @@ def exchange(self, method, path, authority, headers, body, timeout): if flags & FLAG_PADDED: p = p[1:len(p) - bytearray(payload)[0]] resp_body += p + if len(resp_body) > MAX_RESPONSE_SIZE: # cap like the HTTP/1.1 path; stop reading and retire the + self.usable = False # connection (leftover frames abandoned) instead of OOM + break if payload: # replenish stream + connection windows self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, sid, struct.pack("!I", len(payload)))) self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload)))) @@ -603,6 +615,9 @@ def exchange_pair(self, requests, timeout): if flags & FLAG_PRIORITY: p = p[5:] state[fsid]["hb"] += p + if len(state[fsid]["hb"]) > MAX_RESPONSE_SIZE: + self.usable = False + raise IOError("oversized HTTP/2 header block during timeless pair") if flags & FLAG_END_HEADERS: state[fsid]["headers"] = self.dec.decode(state[fsid]["hb"]) if flags & FLAG_END_STREAM and fsid in remaining: @@ -612,6 +627,9 @@ def exchange_pair(self, requests, timeout): if flags & FLAG_PADDED: p = p[1:len(p) - bytearray(payload)[0]] state[fsid]["body"] += p + if len(state[fsid]["body"]) > MAX_RESPONSE_SIZE: # cap the buffered body (mirrors exchange()) + self.usable = False + raise IOError("oversized response during timeless pair") if payload: self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, fsid, struct.pack("!I", len(payload)))) self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload)))) From 8dda2b42a59ac017b01b799cf525ed0a818e0e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 19:09:10 +0200 Subject: [PATCH 2/5] Minor patch --- lib/core/settings.py | 2 +- lib/techniques/union/use.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 0a35a7fce8..3739c21509 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.47" +VERSION = "1.10.7.48" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/techniques/union/use.py b/lib/techniques/union/use.py index e28244c05b..d9b4b589e2 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -131,7 +131,7 @@ def _oneShotUnionUse(expression, unpack=True, limited=False): else: retVal = getUnicode(retVal) elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.H2, DBMS.HSQLDB, DBMS.FIREBIRD): - output = extractRegexResult(r"(?P%s.*%s)" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload)) + output = extractRegexResult(r"(?P%s.*%s)" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload), re.DOTALL) if output: retVal = output else: From d8cc2ae93b95fdda6b9e4f931f44bb115c1a3f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 19:26:36 +0200 Subject: [PATCH 3/5] Minor update --- lib/core/settings.py | 2 +- lib/techniques/union/use.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 3739c21509..19a90dd1e1 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.48" +VERSION = "1.10.7.49" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/techniques/union/use.py b/lib/techniques/union/use.py index d9b4b589e2..86573bdadb 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -31,6 +31,7 @@ from lib.core.common import isNumPosStrValue from lib.core.common import listToStrValue from lib.core.common import parseUnionPage +from lib.core.common import randomStr from lib.core.common import removeReflectiveValues from lib.core.common import singleTimeDebugMessage from lib.core.common import singleTimeWarnMessage @@ -296,7 +297,7 @@ def _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, count offset = 0 while offset < count: - query = "SELECT %s FROM (%s %s) sqmapx" % (aggExpr, base, window(offset, chunk)) + query = "SELECT %s FROM (%s %s) %s" % (aggExpr, base, window(offset, chunk), randomStr()) kb.jsonAggMode = True output = _oneShotUnionUse(query, False) From 5893f069aec124098096d0c007e89c9e752a878c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 9 Jul 2026 08:15:35 +0200 Subject: [PATCH 4/5] Enabling jsonAgg in partial union cases --- lib/core/settings.py | 2 +- lib/core/testing.py | 2 +- lib/techniques/union/use.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 19a90dd1e1..1e0a3c5efa 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.49" +VERSION = "1.10.7.50" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/core/testing.py b/lib/core/testing.py index 758d1a8bf8..bd3f872c0a 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -86,7 +86,7 @@ def vulnTest(tests=None, label="vuln"): ("-u --data=\"aWQ9MQ==\" --flush-session --base64=POST -v 6", ("aWQ9MTtXQUlURk9SIERFTEFZICcwOjA",)), ("-u --flush-session --parse-errors --test-filter=\"subquery\" --eval=\"import hashlib; id2=2; id3=hashlib.md5(id.encode()).hexdigest()\" --referer=\"localhost\"", ("might be injectable", ": syntax error", "back-end DBMS: SQLite", "WHERE or HAVING clause (subquery")), ("-u --technique=BU --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "27 entries", "6E616D6569736E756C6C")), - ("-u --technique=U --fresh-queries --force-partial --dump -T users --dump-format=HTML --answers=\"crack=n\" -v 3", ("performed 31 queries", "nameisnull", "~using default dictionary", "dumped to HTML file")), + ("-u --technique=U --fresh-queries --force-partial --disable-json --dump -T users --dump-format=HTML --answers=\"crack=n\" -v 3", ("LIMIT 0,1)", "nameisnull", "~using default dictionary", "dumped to HTML file")), ("-u --flush-session --technique=BU --all", ("30 entries", "Type: boolean-based blind", "Type: UNION query", "luther", "blisset", "fluffy", "179ad45c6ce2cb97cf1029e212046e81", "NULL", "nameisnull", "testpass")), ("-u --flush-session --technique=B --keyset --dump -T users", ("using keyset (seek) pagination", "30 entries", "luther", "nameisnull")), # keyset/seek dump via the SQLite rowid cursor ("-u -z \"tec=B\" --hex --fresh-queries --threads=4 --sql-query=\"SELECT * FROM users\"", ("SELECT * FROM users [30]", "nameisnull")), diff --git a/lib/techniques/union/use.py b/lib/techniques/union/use.py index 86573bdadb..43de9a31a3 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -348,7 +348,7 @@ def unionUse(expression, unpack=True, dump=False): debugMsg += "it does not play well with UNION query SQL injection" singleTimeDebugMessage(debugMsg) - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL, DBMS.SQLITE, DBMS.H2, DBMS.HSQLDB, DBMS.FIREBIRD) and expressionFields and not any((conf.binaryFields, conf.limitStart, conf.limitStop, conf.forcePartial, conf.disableJson)): + if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL, DBMS.SQLITE, DBMS.H2, DBMS.HSQLDB, DBMS.FIREBIRD) and expressionFields and not any((conf.binaryFields, conf.limitStart, conf.limitStop, conf.disableJson)): match = re.search(r"SELECT\s*(.+?)\bFROM", expression, re.I) if match and not (Backend.isDbms(DBMS.ORACLE) and FROM_DUMMY_TABLE[DBMS.ORACLE] in expression) and not re.search(r"\b(MIN|MAX|COUNT|EXISTS)\(", expression): kb.jsonAggMode = True @@ -374,7 +374,7 @@ def unionUse(expression, unpack=True, dump=False): # response cap) and the table is large, retrieve the rows in bounded windows (chunked # JSON aggregation) before the slow per-row fallback. Done here (independent of the # detected UNION where-clause) so it engages for any dumpable FROM-table query. - if value is None and " FROM " in expression.upper() and not re.search(SQL_SCALAR_REGEX, expression, re.I) and not any((kb.forcePartialUnion, conf.forcePartial, conf.disableJson, conf.binaryFields, conf.limitStart, conf.limitStop)): + if value is None and " FROM " in expression.upper() and not re.search(SQL_SCALAR_REGEX, expression, re.I) and not any((conf.disableJson, conf.binaryFields, conf.limitStart, conf.limitStop)): chunkCountExpr = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % '*', 1) if " ORDER BY " in chunkCountExpr.upper(): chunkCountExpr = chunkCountExpr[:chunkCountExpr.upper().rindex(" ORDER BY ")] From c1516f1750b7a95e9ea92fe96c2fd495c7f65326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 9 Jul 2026 09:02:25 +0200 Subject: [PATCH 5/5] Fixes #6080 --- lib/core/settings.py | 2 +- lib/request/redirecthandler.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 1e0a3c5efa..6fb66aa595 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.50" +VERSION = "1.10.7.51" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/request/redirecthandler.py b/lib/request/redirecthandler.py index 515c415e51..64ee596e09 100644 --- a/lib/request/redirecthandler.py +++ b/lib/request/redirecthandler.py @@ -82,14 +82,20 @@ def http_error_302(self, req, fp, code, msg, headers): redurl = self._get_header_redirect(headers) if not conf.ignoreRedirects else None try: - content = fp.fp.read(MAX_CONNECTION_TOTAL_SIZE) - fp.fp = io.BytesIO(content) + # Note: drain via the length-aware response (honors Content-Length/chunked), NOT the raw + # socket 'fp.fp' - the latter has no HTTP framing, so on a Keep-Alive connection (no EOF) + # it blocks for the whole '--timeout' on every redirect (Issue #6080) + content = fp.read(MAX_CONNECTION_TOTAL_SIZE) except _http_client.IncompleteRead as ex: content = ex.partial - fp.fp = io.BytesIO(content) except: content = b"" + # back 'fp' with the captured body so it stays re-readable downstream (Issue #5985); the + # length-aware read above has consumed the response, so restore both the buffer and read() + fp.fp = io.BytesIO(content) + fp.read = fp.fp.read + content = decodePage(content, headers.get(HTTP_HEADER.CONTENT_ENCODING), headers.get(HTTP_HEADER.CONTENT_TYPE)) threadData = getCurrentThreadData()