From 58d6f740b70e48c2e232a48a40c99e125b773ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 13 Aug 2026 13:26:55 +0200 Subject: [PATCH] Some more patching of dbwire --- extra/dbwire/README.md | 6 +- extra/dbwire/__init__.py | 48 ++++++++++-- extra/dbwire/cubrid.py | 44 +++++------ extra/dbwire/firebird.py | 87 ++++++++++------------ extra/dbwire/monetdb.py | 25 +++---- extra/dbwire/mysql.py | 40 +++++----- extra/dbwire/postgres.py | 22 ++---- extra/dbwire/tds.py | 32 ++++---- lib/core/settings.py | 2 +- tests/test_dbwire.py | 157 ++++++++++++++++++++++++++++++++++++--- 10 files changed, 297 insertions(+), 166 deletions(-) diff --git a/extra/dbwire/README.md b/extra/dbwire/README.md index cfd37dceb..ea9f0a305 100644 --- a/extra/dbwire/README.md +++ b/extra/dbwire/README.md @@ -22,7 +22,7 @@ A wire protocol is shared across a whole family of products, so one client serve |-----------------|---------------------|---------| | `postgres.py` | PostgreSQL v3 | PostgreSQL, CockroachDB, CrateDB, Redshift, Greenplum, Vertica | | `mysql.py` | MySQL client/server | MySQL, MariaDB, TiDB, Aurora (MySQL), Percona | -| `tds.py` | TDS | Microsoft SQL Server, Sybase | +| `tds.py` | TDS 7.x | Microsoft SQL Server | | `firebird.py` | Firebird wire | Firebird 3 / 4 / 5 | | `cubrid.py` | CUBRID CAS | CUBRID | | `clickhouse.py` | HTTP (TabSeparated) | ClickHouse and HTTP-compatible forks | @@ -57,7 +57,9 @@ parameter binding, prepared statements, bulk load/`COPY`, or TLS. Notable per-pr `caching_sha2_password` authentication over a plaintext connection requires RSA/TLS and is not supported; use a `mysql_native_password` account for the dependency-free path. - **TDS** - cleartext login only. Servers that force encryption (for example Azure SQL Database) - require TLS and are not supported here; the native driver or SQLAlchemy tier covers those. + require TLS and are not supported here; the native driver or SQLAlchemy tier covers those. LOGIN7 is the + Microsoft dialect (TDS 7.x); Sybase ASE speaks TDS 5.0 (LOGINREC + its own token dialect), which is not + implemented yet - ASE `-d` goes through the pymssql/FreeTDS tier for now. - **Firebird** - SRP-256 (and SRP) authentication with ChaCha20 or RC4 wire encryption, as required by default on Firebird 3 and later. Legacy (pre-SRP) authentication is not implemented. - **CUBRID** - cleartext login over the CAS broker protocol. Large objects (BLOB/CLOB) are returned as diff --git a/extra/dbwire/__init__.py b/extra/dbwire/__init__.py index e809c201d..4f3c4c32b 100644 --- a/extra/dbwire/__init__.py +++ b/extra/dbwire/__init__.py @@ -11,10 +11,12 @@ sqlmap's direct ('-d') connection when no native driver (and no SQLAlchemy) is i Design note: connectors speak a *wire protocol*, not a product, so a single client covers the whole compatible family - e.g. the PostgreSQL client also serves CockroachDB, CrateDB, Redshift and Greenplum; -a MySQL client serves MariaDB/TiDB/Aurora; a TDS client serves MSSQL/Sybase. Each module exposes a small -PEP 249 (DB-API 2.0) subset (connect(), Connection.cursor()/commit()/close(), Cursor.execute()/fetchall()). +a MySQL client serves MariaDB/TiDB/Aurora. Each module exposes a small PEP 249 (DB-API 2.0) subset +(connect(), Connection.cursor()/commit()/close(), Cursor.execute()/fetchall()). """ +import socket + __version__ = "0.1" apilevel = "2.0" @@ -71,6 +73,21 @@ def connection_lost(ex): return OperationalError("connection lost (%s)" % ex) +def handshake_done(sock): + """ + Drop the connect deadline, once the login exchange is over. + + connect_timeout has to stay armed THROUGH the handshake, not just the TCP connect: a peer that + accepts the connection and then says nothing (a wrong port, a silent proxy, a dropping firewall) is + perfectly alive as far as keepalive() below is concerned, so an unbounded login read waits forever. + A query is the opposite case - see keepalive(). + """ + + try: + sock.settimeout(None) + except Exception: + pass + def keepalive(sock): """ Ask the kernel to probe an idle connection, so a peer that dies without a FIN is eventually detected. @@ -80,12 +97,29 @@ def keepalive(sock): failure being guarded against. Best-effort - the options are not portable everywhere. """ - import socket as _socket - try: - sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_KEEPALIVE, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) for name, value in (("TCP_KEEPIDLE", 60), ("TCP_KEEPINTVL", 10), ("TCP_KEEPCNT", 5)): - if hasattr(_socket, name): - sock.setsockopt(_socket.IPPROTO_TCP, getattr(_socket, name), value) + if hasattr(socket, name): + sock.setsockopt(socket.IPPROTO_TCP, getattr(socket, name), value) except Exception: pass + +def recvn(sock, n): + """ + Read exactly n bytes off `sock`, or raise - every wire protocol here is framed, so a short read is a + desynchronized stream, not a smaller message. + + Shared because it was five identical copies: a fix to the recv loop has to land once, not per module. + """ + + buf = b"" + while len(buf) < n: + try: + chunk = sock.recv(n - len(buf)) + except (socket.error, OSError) as ex: + raise connection_lost(ex) + if not chunk: + raise InterfaceError("connection closed by server") + buf += chunk + return buf diff --git a/extra/dbwire/cubrid.py b/extra/dbwire/cubrid.py index 4aba46131..aea26d480 100644 --- a/extra/dbwire/cubrid.py +++ b/extra/dbwire/cubrid.py @@ -24,7 +24,9 @@ from extra.dbwire import IntegrityError from extra.dbwire import NotSupportedError from extra.dbwire import OperationalError from extra.dbwire import connection_lost +from extra.dbwire import handshake_done from extra.dbwire import keepalive +from extra.dbwire import recvn from extra.dbwire import ProgrammingError _MAGIC = b"CUBRK" @@ -261,23 +263,13 @@ class Connection(object): self._sock = None def _recvn(self, n): - buf = b"" - while len(buf) < n: - try: - chunk = self._sock.recv(n - len(buf)) - except (socket.error, OSError) as ex: - raise connection_lost(ex) - if not chunk: - raise InterfaceError("connection closed by server") - buf += chunk - return buf + return recvn(self._sock, n) def _open(self): # broker handshake (may redirect to a dedicated CAS worker port), then cleartext OPEN_DATABASE login try: sock = socket.create_connection((self._host, self._port), timeout=self._timeout) keepalive(sock) - sock.settimeout(None) sock.sendall(_MAGIC + struct.pack(">BB", _CLIENT_JDBC, _CAS_VERSION) + b"\x00\x00\x00") self._sock = sock (port,) = struct.unpack(">i", self._recvn(4)) @@ -287,21 +279,25 @@ class Connection(object): self._safe_close() sock = socket.create_connection((self._host, port), timeout=self._timeout) keepalive(sock) - sock.settimeout(None) self._sock = sock + + login = self._fixed(self._database, 32) + self._fixed(self._user, 32) + self._fixed(self._password, 32) + login += b"\x00" * 532 # 512 extended-info + 20 reserved + self._sock.sendall(login) + reader = self._read_response() + reader.int() # response_code (>=0; errors already raised in _read_response) + broker = reader.raw(8) + self._protocol_version = bytearray(broker)[4] & 0x3f + # enable auto-commit so each statement is independent (avoids the CAS keep-connection handshake dance) + self._call(_Writer(_FC_SET_DB_PARAMETER).arg_int(_PARAM_AUTO_COMMIT).arg_int(1)) except (socket.error, socket.timeout) as ex: self._safe_close() raise OperationalError("could not connect to '%s:%s' (%s)" % (self._host, self._port, ex)) + except Exception: # a rejected login (or connection_lost() out of _recvn) is still ours to close + self._safe_close() + raise - login = self._fixed(self._database, 32) + self._fixed(self._user, 32) + self._fixed(self._password, 32) - login += b"\x00" * 532 # 512 extended-info + 20 reserved - self._sock.sendall(login) - reader = self._read_response() - reader.int() # response_code (>=0; errors already raised in _read_response) - broker = reader.raw(8) - self._protocol_version = bytearray(broker)[4] & 0x3f - # enable auto-commit so each statement is independent (avoids the CAS keep-connection handshake dance) - self._call(_Writer(_FC_SET_DB_PARAMETER).arg_int(_PARAM_AUTO_COMMIT).arg_int(1)) + handshake_done(self._sock) @staticmethod def _fixed(value, length): @@ -347,9 +343,9 @@ class Connection(object): text = message.lower() if any(k in text for k in ("unique", "duplicate", "foreign key", "constraint violat")): raise IntegrityError(message) - if any(k in text for k in ("syntax", "unknown class", "does not exist", "not found", "before ' '")): - raise ProgrammingError(message) - if any(k in text for k in ("cast", "conversion", "overflow", "truncat")): + if any(k in text for k in ("syntax", "unknown class", "does not exist", "not found", "before '")): + raise ProgrammingError(message) # CUBRID points at the offending token as: before ' ,'y')' + if any(k in text for k in ("cast", "coerce", "conversion", "overflow", "truncat")): raise DataError(message) raise ProgrammingError(message) diff --git a/extra/dbwire/firebird.py b/extra/dbwire/firebird.py index 218fbf46a..b0db9f6e1 100644 --- a/extra/dbwire/firebird.py +++ b/extra/dbwire/firebird.py @@ -27,7 +27,9 @@ from extra.dbwire import InterfaceError from extra.dbwire import NotSupportedError from extra.dbwire import OperationalError from extra.dbwire import connection_lost +from extra.dbwire import handshake_done from extra.dbwire import keepalive +from extra.dbwire import recvn # operation codes _op_connect = 1 @@ -296,16 +298,7 @@ class _Wire(object): raise connection_lost(ex) def _recv_raw(self, n): - buf = b"" - while len(buf) < n: - try: - chunk = self._sock.recv(n - len(buf)) - except (socket.error, OSError) as ex: - raise connection_lost(ex) - if not chunk: - raise InterfaceError("connection closed by server") - buf += chunk - return buf + return recvn(self._sock, n) def recv(self, n, align=False): # every length here comes off the wire (response buffers, status strings, per-value lengths): a @@ -330,6 +323,38 @@ class _Wire(object): except Exception: pass +def _parse_response(wire): + head = wire.recv(16) + handle = struct.unpack("!i", head[:4])[0] + object_id = head[4:12] + buf = wire.recv(struct.unpack("!i", head[12:16])[0], align=True) + _check_status(wire) + return handle, object_id, buf + +def _check_status(wire): + gds, message = set(), "" + n = wire.recv_int() + while n != _isc_arg_end: + if n == _isc_arg_gds: + gds_code = wire.recv_int() + if gds_code: + gds.add(gds_code) + elif n == _isc_arg_number: + message += " %d" % wire.recv_int() + elif n in (_isc_arg_string, _isc_arg_interpreted, _isc_arg_sql_state): + s = wire.recv(wire.recv_int(), align=True) + if n != _isc_arg_sql_state: + message += " " + s.decode("utf-8", "replace") + n = wire.recv_int() + if gds: + message = ("(remote) firebird error %s%s" % (sorted(gds), message)).strip() + if gds & _GDS_INTEGRITY: + raise IntegrityError(message) + if gds & _GDS_DATA: + raise DataError(message) + if _GDS_WARNING not in gds: + raise OperationalError(message) + def _pack_int(v): return struct.pack("!i", v) @@ -461,39 +486,7 @@ class Connection(object): op = self._wire.recv_int() if op != _op_response: raise OperationalError("unexpected Firebird operation %d" % op) - return self._parse_response() - - def _parse_response(self): - head = self._wire.recv(16) - handle = struct.unpack("!i", head[:4])[0] - object_id = head[4:12] - buf = self._wire.recv(struct.unpack("!i", head[12:16])[0], align=True) - self._check_status() - return handle, object_id, buf - - def _check_status(self): - gds, message = set(), "" - n = self._wire.recv_int() - while n != _isc_arg_end: - if n == _isc_arg_gds: - gds_code = self._wire.recv_int() - if gds_code: - gds.add(gds_code) - elif n == _isc_arg_number: - message += " %d" % self._wire.recv_int() - elif n in (_isc_arg_string, _isc_arg_interpreted, _isc_arg_sql_state): - s = self._wire.recv(self._wire.recv_int(), align=True) - if n != _isc_arg_sql_state: - message += " " + s.decode("utf-8", "replace") - n = self._wire.recv_int() - if gds: - message = ("(remote) firebird error %s%s" % (sorted(gds), message)).strip() - if gds & _GDS_INTEGRITY: - raise IntegrityError(message) - if gds & _GDS_DATA: - raise DataError(message) - if _GDS_WARNING not in gds: - raise OperationalError(message) + return _parse_response(self._wire) # ---- query ---- @@ -632,7 +625,7 @@ class Connection(object): op = self._wire.recv_int() if op != _op_fetch_response: if op == _op_response: - self._parse_response() + _parse_response(self._wire) raise OperationalError("unexpected Firebird operation %d during fetch" % op) status = self._wire.recv_int() count = self._wire.recv_int() @@ -765,7 +758,6 @@ def connect(host=None, port=3050, user=None, password=None, database=None, conne try: sock = socket.create_connection((host or "localhost", int(port or 3050)), timeout=connect_timeout) keepalive(sock) - sock.settimeout(None) except (socket.error, socket.timeout) as ex: raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex)) @@ -782,6 +774,7 @@ def connect(host=None, port=3050, user=None, password=None, database=None, conne _authenticate(wire, user, password, public_key, private_key) connection = Connection(wire, filename, user, password) _attach(connection, wire, user) + handshake_done(sock) except (DatabaseError, InterfaceError): wire.close() raise @@ -805,7 +798,7 @@ def _authenticate(wire, user, password, public_key, private_key): if op == _op_reject: raise OperationalError("Firebird connection rejected") if op == _op_response: - Connection(wire, b"", user, password)._parse_response() # will raise the server error + _parse_response(wire) # will raise the server error raise OperationalError("Firebird connection rejected") wire.recv(12) # accept block: protocol version / architecture / type (not needed once lazy-send is off) @@ -858,7 +851,7 @@ def _read_response(wire, user, password): raise OperationalError("Firebird authentication failed") if op != _op_response: raise OperationalError("unexpected Firebird operation %d during login" % op) - return Connection(wire, b"", user, password)._parse_response()[2] + return _parse_response(wire)[2] def _attach(connection, wire, user): dpb = bytearray([_isc_dpb_version1]) diff --git a/extra/dbwire/monetdb.py b/extra/dbwire/monetdb.py index b51b031a3..0652811cf 100644 --- a/extra/dbwire/monetdb.py +++ b/extra/dbwire/monetdb.py @@ -23,35 +23,25 @@ from extra.dbwire import InterfaceError from extra.dbwire import NotSupportedError from extra.dbwire import OperationalError from extra.dbwire import connection_lost +from extra.dbwire import handshake_done from extra.dbwire import keepalive +from extra.dbwire import recvn from extra.dbwire import ProgrammingError _MAX_BLOCK = 0xffff >> 1 _MAX_MESSAGE_LENGTH = 0x40000000 # cap on a (re-assembled) response, to bound a hostile/corrupt stream -def _recvn(sock, n): - buf = b"" - while len(buf) < n: - try: - chunk = sock.recv(n - len(buf)) - except (socket.error, OSError) as ex: - raise connection_lost(ex) - if not chunk: - raise InterfaceError("connection closed by server") - buf += chunk - return buf - def _getblock(sock): # the block length is a 15-bit field, so bounding IT is pointless - a peer that never sets the last-flag # simply streams blocks forever. Bound the accumulated response instead (as the other wire modules do). chunks, total = [], 0 while True: - (header,) = struct.unpack("> 1, header & 1 total += length if total > _MAX_MESSAGE_LENGTH: raise InterfaceError("backend message too large (%d bytes)" % total) - chunks.append(_recvn(sock, length)) + chunks.append(recvn(sock, length)) if last: break return b"".join(chunks).decode("utf-8", "replace") @@ -201,7 +191,6 @@ def connect(host=None, port=50000, user=None, password=None, database=None, conn try: sock = socket.create_connection((host, port), timeout=connect_timeout) keepalive(sock) - sock.settimeout(None) except (socket.error, socket.timeout) as ex: raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex)) @@ -218,7 +207,6 @@ def connect(host=None, port=50000, user=None, password=None, database=None, conn host, port, database = m.group(1), int(m.group(2)), m.group(3) or database sock = socket.create_connection((host, port), timeout=connect_timeout) keepalive(sock) - sock.settimeout(None) continue # merovingian proxy redirect: keep reading the next challenge on this socket if block[0] == "!": raise OperationalError("(remote) %s" % block[1:].strip()) @@ -244,4 +232,9 @@ def connect(host=None, port=50000, user=None, password=None, database=None, conn except (socket.error, socket.timeout) as ex: connection.close() raise OperationalError("connection error: %s" % ex) + except Exception: # e.g. connection_lost() out of _putblock/_getblock: the socket is still ours to close + connection.close() + raise + + handshake_done(sock) return connection diff --git a/extra/dbwire/mysql.py b/extra/dbwire/mysql.py index b7d119610..e6a923aa5 100644 --- a/extra/dbwire/mysql.py +++ b/extra/dbwire/mysql.py @@ -23,7 +23,9 @@ from extra.dbwire import InterfaceError from extra.dbwire import NotSupportedError from extra.dbwire import OperationalError from extra.dbwire import connection_lost +from extra.dbwire import handshake_done from extra.dbwire import keepalive +from extra.dbwire import recvn from extra.dbwire import ProgrammingError # capability flags @@ -62,31 +64,19 @@ def _cstring(data, off): return data[off:], len(data) return data[off:end], end + 1 -def _recvn(sock, n): - buf = b"" - while len(buf) < n: - try: - chunk = sock.recv(n - len(buf)) - except (socket.error, OSError) as ex: - raise connection_lost(ex) - if not chunk: - raise InterfaceError("connection closed by server") - buf += chunk - return buf - def _read_packet(sock): - header = _recvn(sock, 4) + header = recvn(sock, 4) length = struct.unpack(" _MAX_MESSAGE_LENGTH: raise InterfaceError("backend message too large (%d bytes)" % total) - payload += _recvn(sock, length) + payload += recvn(sock, length) return seq, payload def _send_packet(sock, seq, payload): @@ -295,7 +285,6 @@ def connect(host=None, port=3306, user=None, password=None, database=None, conne try: sock = socket.create_connection((host or "localhost", int(port or 3306)), timeout=connect_timeout) keepalive(sock) - sock.settimeout(None) except (socket.error, socket.timeout) as ex: raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex)) @@ -352,6 +341,7 @@ def connect(host=None, port=3306, user=None, password=None, database=None, conne _send_packet(sock, seq + 1, response) _finish_auth(sock, password or "", plugin or "mysql_native_password", salt) + handshake_done(sock) except (DatabaseError, InterfaceError): _safe_close(sock) raise @@ -363,12 +353,16 @@ def connect(host=None, port=3306, user=None, password=None, database=None, conne # SET NAMES: reset collation_connection to the server's default (the fixed handshake collation 45 = # utf8mb4_general_ci otherwise clashes with MySQL 8's utf8mb4_0900_ai_ci columns -> 'illegal mix of # collations' 1271 in a UNION/CONCAT); results stay utf8mb4 so the utf-8 decode is unchanged. autocommit=1 - # so DML persists even if the server default is autocommit=0. Both best-effort (one-time, at connect). - for setup in ("SET NAMES utf8mb4", "SET autocommit=1"): - try: - connection._query(setup) - except Exception: - pass + # so DML persists even if the server default is autocommit=0. Best-effort (one-time, at connect) - but the + # charset has to land on *something*, else rows decode as utf-8 that never was: pre-5.5.3 servers have no + # utf8mb4, and 'utf8' (3-byte) is the fallback every 4.1+ server does have. + for alternatives in (("SET NAMES utf8mb4", "SET NAMES utf8"), ("SET autocommit=1",)): + for setup in alternatives: + try: + connection._query(setup) + break + except Exception: + pass return connection def _safe_close(sock): diff --git a/extra/dbwire/postgres.py b/extra/dbwire/postgres.py index db576122e..30d36ca8b 100644 --- a/extra/dbwire/postgres.py +++ b/extra/dbwire/postgres.py @@ -29,7 +29,9 @@ from extra.dbwire import InterfaceError from extra.dbwire import NotSupportedError from extra.dbwire import OperationalError from extra.dbwire import connection_lost +from extra.dbwire import handshake_done from extra.dbwire import keepalive +from extra.dbwire import recvn from extra.dbwire import ProgrammingError _PROTOCOL_VERSION = 196608 # 3.0 @@ -70,24 +72,12 @@ def _xor(a, b): return b"".join(chr(ord(x) ^ ord(y)) for x, y in zip(a, b)) return bytes(x ^ y for x, y in zip(a, b)) -def _recvn(sock, n): - buf = b"" - while len(buf) < n: - try: - chunk = sock.recv(n - len(buf)) - except (socket.error, OSError) as ex: - raise connection_lost(ex) - if not chunk: - raise InterfaceError("connection closed by server") - buf += chunk - return buf - def _read_message(sock): - mtype = _recvn(sock, 1) - (length,) = struct.unpack("!I", _recvn(sock, 4)) + mtype = recvn(sock, 1) + (length,) = struct.unpack("!I", recvn(sock, 4)) if length < 4 or length > _MAX_MESSAGE_LENGTH: raise InterfaceError("invalid backend message length (%d)" % length) - return mtype, _recvn(sock, length - 4) + return mtype, recvn(sock, length - 4) def _send(sock, mtype, payload): try: @@ -313,7 +303,6 @@ def connect(host=None, port=5432, user=None, password=None, database=None, conne try: sock = socket.create_connection((host or "localhost", int(port or 5432)), timeout=connect_timeout) keepalive(sock) - sock.settimeout(None) except (socket.error, socket.timeout) as ex: raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex)) @@ -331,6 +320,7 @@ def connect(host=None, port=5432, user=None, password=None, database=None, conne _raise_server_error_as_operational(payload) if mtype == b"Z": break + handshake_done(sock) except Exception: # any setup failure (DB-API or otherwise) must still close the socket try: sock.close() diff --git a/extra/dbwire/tds.py b/extra/dbwire/tds.py index 3cad7d70a..ecc41bc9b 100644 --- a/extra/dbwire/tds.py +++ b/extra/dbwire/tds.py @@ -6,7 +6,10 @@ See the file 'LICENSE' for copying permission """ """ -Minimal pure-python TDS (Tabular Data Stream) client for Microsoft SQL Server / Sybase (stdlib only). +Minimal pure-python TDS (Tabular Data Stream) client for Microsoft SQL Server (stdlib only). + +LOGIN7 / TDS 7.4 is the Microsoft dialect. Sybase ASE speaks TDS 5.0 - same 8-byte packet framing, but a +LOGINREC login and its own token/type dialect - which is not implemented yet, so ASE still needs pymssql. Cleartext login only (TDS pre-login encryption negotiated to NOT_SUP); a server that forces encryption would need TLS-in-TDS which is out of scope for the dependency-free client. Implements PRELOGIN, LOGIN7, @@ -23,7 +26,9 @@ from extra.dbwire import InterfaceError from extra.dbwire import NotSupportedError from extra.dbwire import OperationalError from extra.dbwire import connection_lost +from extra.dbwire import handshake_done from extra.dbwire import keepalive +from extra.dbwire import recvn from extra.dbwire import ProgrammingError _MAX_MESSAGE_LENGTH = 0x40000000 @@ -38,18 +43,6 @@ _STATUS_EOM = 0x01 def _u8(data, off): return struct.unpack("BBH", header[:4]) if length < 8: raise InterfaceError("invalid TDS packet length (%d)" % length) total += length - 8 if total > _MAX_MESSAGE_LENGTH: raise InterfaceError("TDS message exceeds the maximum allowed length (%d bytes)" % _MAX_MESSAGE_LENGTH) - chunks.append(_recvn(sock, length - 8)) + chunks.append(recvn(sock, length - 8)) if status & _STATUS_EOM: break return b"".join(chunks) @@ -160,7 +153,10 @@ def _login7(sock, user, password, database, hostname="dbwire", appname="dbwire") # ---- token stream + type decoding -------------------------------------------------------------------- -def _read_us_varchar(data, off): +def _read_b_varchar(data, off): + # B_VARCHAR: 1-byte length in UCS-2 *characters* (not bytes), then that many 16-bit units. COLMETADATA's + # ColName is a B_VARCHAR (MS-TDS 2.2.7.4) - identifiers cap at 128 chars and an unaliased expression + # column comes back with an empty name, so the one-byte count cannot overflow. (n,) = struct.unpack("...) -VERSION = "1.10.8.30" +VERSION = "1.10.8.31" 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/tests/test_dbwire.py b/tests/test_dbwire.py index 2d3b68370..a1e8c740d 100644 --- a/tests/test_dbwire.py +++ b/tests/test_dbwire.py @@ -30,6 +30,7 @@ from _testutils import bootstrap bootstrap() import extra.dbwire as dbwire +from extra.dbwire import clickhouse as _clickhouse from extra.dbwire import connection_lost from extra.dbwire import cubrid as _cubrid from extra.dbwire import firebird as _firebird @@ -38,6 +39,7 @@ from extra.dbwire import monetdb as _monetdb from extra.dbwire import mysql as _mysql from extra.dbwire import postgres as _postgres from extra.dbwire import presto as _presto +from extra.dbwire import recvn from extra.dbwire import tds as _tds @@ -47,6 +49,7 @@ class FakeSocket(object): def __init__(self, inbound=b""): self.inbound = bytearray(inbound) self.sent = bytearray() + self.timeouts = [] self.closed = False def feed(self, data): @@ -62,8 +65,8 @@ class FakeSocket(object): def sendall(self, data): self.sent.extend(data) - def settimeout(self, _value): - pass + def settimeout(self, value): + self.timeouts.append(value) def setsockopt(self, *_args): pass @@ -180,20 +183,23 @@ class PostgresScramTest(unittest.TestCase): self.assertRaises(dbwire.OperationalError, _postgres._authenticate, sock, "user", "secret") -class MysqlCapabilityTest(unittest.TestCase): - def _handshake(self, server_caps): - payload = b"\x0a" + b"8.0.0-fake\x00" + struct.pack("> 16) & 0xffff) - payload += struct.pack("> 16) & 0xffff) + payload += struct.pack("BBHHBB", 4, 1 if eom else 0, len(body) + 8, 0, 0, 0) + body @@ -387,6 +456,61 @@ class BoundedReadTest(unittest.TestCase): _monetdb._MAX_MESSAGE_LENGTH = saved +class DecoderTest(unittest.TestCase): + """Byte fixtures for the socket-free decoders, taken from what the real servers put on the wire.""" + + def test_tds_column_name_is_a_character_count(self): + """COLMETADATA ColName is B_VARCHAR - a length in UCS-2 characters (MS-TDS 2.2.7.4), so the count + byte is followed by twice as many bytes.""" + + data = struct.pack("'"; a coercion failure is a DataError.""" + + for message, expected in (("(remote) Syntax: In line 1, column 1 before ' 1'", dbwire.ProgrammingError), + ("(remote) Unknown class \"public.t\".", dbwire.ProgrammingError), + ("(remote) Cannot coerce value of domain \"character\" to domain \"integer\".", + dbwire.DataError), + ("(remote) unique constraint violated", dbwire.IntegrityError)): + self.assertRaises(expected, _cubrid.Connection._raise, -494, message) + + class HelperTest(unittest.TestCase): def test_socket_failure_maps_into_the_dbapi_hierarchy(self): """Callers of a PEP 249 driver only catch Error and its subclasses.""" @@ -400,6 +524,15 @@ class HelperTest(unittest.TestCase): self.assertEqual(http_origin("[fe80::1]", 8123), "http://[fe80::1]:8123") self.assertEqual(http_origin(None, 8123), "http://localhost:8123") + def test_recvn_reads_exactly_n_bytes_across_chunks(self): + """Shared by every socket module: a framed protocol has no notion of a short read.""" + + sock = FakeSocket(b"ABCDEFGH") + original = sock.recv + sock.recv = lambda _count: original(3) # dribble the stream 3 bytes at a time + self.assertEqual(recvn(sock, 8), b"ABCDEFGH") + self.assertRaises(dbwire.InterfaceError, recvn, FakeSocket(b"AB"), 4) + def test_every_module_exposes_the_dbapi_surface(self): for name in ("postgres", "mysql", "tds", "firebird", "cubrid", "monetdb", "clickhouse", "presto"): module = __import__("extra.dbwire.%s" % name, fromlist=["connect"])