| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -132,6 +132,10 @@ def double(self): | |||
| 132 | 132 | v = struct.unpack_from(">d", self._buf, self._off)[0]; self._off += 8; return v | |
| 133 | 133 | ||
| 134 | 134 | def raw(self, n): | |
| 135 | + # a slice past the end returns short data *silently*, which would hand a truncated value to the | ||
| 136 | + # caller (the fixed-width readers above raise struct.error instead) | ||
| 137 | + if n < 0 or n > self.remaining(): | ||
| 138 | + raise InterfaceError("CAS response too short (wanted %d of %d bytes)" % (n, self.remaining())) | ||
| 135 | 139 | v = self._buf[self._off:self._off + n]; self._off += n; return bytes(v) | |
| 136 | 140 | ||
| 137 | 141 | def skip(self, n): | |
@@ -371,9 +375,15 @@ def _execute(self, handle, reader): | |||
| 371 | 375 | reader.byte() # is_updatable | |
| 372 | 376 | columns = self._parse_columns(reader, reader.int()) | |
| 373 | 377 | ||
| 378 | + # args: handle, flag, max_col_size, max_row, binds, fetch_flag, auto_commit, forward_only_cursor, | ||
| 379 | + # cache_time, query_timeout. auto_commit makes the CAS worker commit the statement and end the | ||
| 380 | + # transaction - without it DML is rolled back when the connection drops, whatever _open() asked | ||
| 381 | + # SET_DB_PARAMETER for. It must stay off for a SELECT: ending the transaction there invalidates the | ||
| 382 | + # request handle the paged _fetch_remaining() still reads from. | ||
| 383 | + select = stmt_type == _STMT_SELECT | ||
| 374 | 384 | exec_writer = (_Writer(_FC_EXECUTE).arg_int(handle).arg_byte(0).arg_int(0).arg_int(0) | |
| 375 | - .arg_null().arg_byte(1 if stmt_type == _STMT_SELECT else 0) | ||
| 376 | - .arg_byte(0).arg_byte(1).arg_cache_time().arg_int(0)) | ||
| 385 | + .arg_null().arg_byte(1 if select else 0) | ||
| 386 | + .arg_byte(0 if select else 1).arg_byte(1).arg_cache_time().arg_int(0)) | ||
| 377 | 387 | reader = self._call(exec_writer) | |
| 378 | 388 | ||
| 379 | 389 | total = reader.int() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -142,6 +142,8 @@ | |||
| 142 | 142 | _GDS_DATA = frozenset((335544321,)) | |
| 143 | 143 | _GDS_WARNING = 335544434 | |
| 144 | 144 | ||
| 145 | + _MAX_MESSAGE_LENGTH = 0x40000000 # cap on a wire-supplied length, to bound a hostile/corrupt stream | ||
| 146 | + | ||
| 145 | 147 | # SRP-6a group used by Firebird (fixed 1024-bit prime, generator 2) | |
| 146 | 148 | _SRP_N = int("E67D2E994B2F900C3F41F08F5BB2627ED0D49EE1FE767A52EFCD565CD6E768812C3E1E9CE8F0A8BEA6CB13CD29DDE" | |
| 147 | 149 | "BF7A96D4A93B55D488DF099A15C89DCB0640738EB2CBDD9A8F7BAB561AB1B0DC1C6CDABF303264A08D1BCA932D1F" | |
@@ -306,6 +308,10 @@ def _recv_raw(self, n): | |||
| 306 | 308 | return buf | |
| 307 | 309 | ||
| 308 | 310 | def recv(self, n, align=False): | |
| 311 | + # every length here comes off the wire (response buffers, status strings, per-value lengths): a | ||
| 312 | + # negative one would silently return short data, a huge one would read until memory ran out | ||
| 313 | + if n < 0 or n > _MAX_MESSAGE_LENGTH: | ||
| 314 | + raise InterfaceError("invalid Firebird length (%d)" % n) | ||
| 309 | 315 | total = n + ((4 - n % 4) % 4) if align else n | |
| 310 | 316 | data = self._recv_raw(total) | |
| 311 | 317 | if self._rc: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -27,6 +27,7 @@ | |||
| 27 | 27 | from extra.dbwire import ProgrammingError | |
| 28 | 28 | ||
| 29 | 29 | _MAX_BLOCK = 0xffff >> 1 | |
| 30 | + _MAX_MESSAGE_LENGTH = 0x40000000 # cap on a (re-assembled) response, to bound a hostile/corrupt stream | ||
| 30 | 31 | ||
| 31 | 32 | def _recvn(sock, n): | |
| 32 | 33 | buf = b"" | |
@@ -41,14 +42,19 @@ def _recvn(sock, n): | |||
| 41 | 42 | return buf | |
| 42 | 43 | ||
| 43 | 44 | def _getblock(sock): | |
| 44 | - out = b"" | ||
| 45 | + # the block length is a 15-bit field, so bounding IT is pointless - a peer that never sets the last-flag | ||
| 46 | + # simply streams blocks forever. Bound the accumulated response instead (as the other wire modules do). | ||
| 47 | + chunks, total = [], 0 | ||
| 45 | 48 | while True: | |
| 46 | 49 | (header,) = struct.unpack("<H", _recvn(sock, 2)) | |
| 47 | 50 | length, last = header >> 1, header & 1 | |
| 48 | - out += _recvn(sock, length) | ||
| 51 | + total += length | ||
| 52 | + if total > _MAX_MESSAGE_LENGTH: | ||
| 53 | + raise InterfaceError("backend message too large (%d bytes)" % total) | ||
| 54 | + chunks.append(_recvn(sock, length)) | ||
| 49 | 55 | if last: | |
| 50 | 56 | break | |
| 51 | - return out.decode("utf-8", "replace") | ||
| 57 | + return b"".join(chunks).decode("utf-8", "replace") | ||
| 52 | 58 | ||
| 53 | 59 | def _putblock(sock, text): | |
| 54 | 60 | data = text.encode("utf-8") | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -321,9 +321,9 @@ def connect(host=None, port=5432, user=None, password=None, database=None, conne | |||
| 321 | 321 | for key, value in (("user", user or ""), ("database", database or user or ""), ("client_encoding", "UTF8")): | |
| 322 | 322 | params += key.encode("ascii") + b"\x00" + ("%s" % value).encode("utf-8") + b"\x00" | |
| 323 | 323 | params += b"\x00" | |
| 324 | - _send(sock, b"", struct.pack("!I", _PROTOCOL_VERSION) + params) | ||
| 325 | 324 | ||
| 326 | 325 | try: | |
| 326 | + _send(sock, b"", struct.pack("!I", _PROTOCOL_VERSION) + params) # StartupMessage | ||
| 327 | 327 | _authenticate(sock, user, password) | |
| 328 | 328 | while True: # drain until ReadyForQuery (ParameterStatus/BackendKeyData/NoticeResponse) | |
| 329 | 329 | mtype, payload = _read_message(sock) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -20,7 +20,7 @@ | |||
| 20 | 20 | from thirdparty import six | |
| 21 | 21 | ||
| 22 | 22 | # sqlmap version (<major>.<minor>.<month>.<monthly commit>) | |
| 23 | - VERSION = "1.10.8.29" | ||
| 23 | + VERSION = "1.10.8.30" | ||
| 24 | 24 | TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" | |
| 25 | 25 | TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} | |
| 26 | 26 | VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -31,7 +31,10 @@ | |||
| 31 | 31 | ||
| 32 | 32 | import extra.dbwire as dbwire | |
| 33 | 33 | from extra.dbwire import connection_lost | |
| 34 | + from extra.dbwire import cubrid as _cubrid | ||
| 35 | + from extra.dbwire import firebird as _firebird | ||
| 34 | 36 | from extra.dbwire import http_origin | |
| 37 | + from extra.dbwire import monetdb as _monetdb | ||
| 35 | 38 | from extra.dbwire import mysql as _mysql | |
| 36 | 39 | from extra.dbwire import postgres as _postgres | |
| 37 | 40 | from extra.dbwire import presto as _presto | |
@@ -314,6 +317,76 @@ def test_schema_is_never_sent_without_a_catalog(self): | |||
| 314 | 317 | self.assertNotIn("X-Presto-Schema", c._headers) | |
| 315 | 318 | ||
| 316 | 319 | ||
| 320 | + class CubridAutoCommitTest(unittest.TestCase): | ||
| 321 | + """CUBRID has no server-side autocommit switch that outlives a statement: the EXECUTE request carries | ||
| 322 | + its own auto_commit byte, and a statement sent with it clear is rolled back when the CAS worker goes | ||
| 323 | + away. It must stay clear for a SELECT, whose request handle the paged fetch still reads from.""" | ||
| 324 | + | ||
| 325 | + def _auto_commit_byte(self, stmt_type): | ||
| 326 | + connection = object.__new__(_cubrid.Connection) | ||
| 327 | + connection._protocol_version = 8 | ||
| 328 | + connection._sock = None | ||
| 329 | + sent = [] | ||
| 330 | + | ||
| 331 | + def _call(writer): | ||
| 332 | + sent.append(writer.payload()) | ||
| 333 | + # EXECUTE response: total(4) cache_reusable(1) result_count(4) includes_column_info(1) shard_id(4) | ||
| 334 | + return _cubrid._Reader(struct.pack(">iBiBi", 0, 0, 0, 0, 0)) | ||
| 335 | + | ||
| 336 | + connection._call = _call | ||
| 337 | + prepare = _cubrid._Reader(struct.pack(">iBiBi", 0, stmt_type, 0, 0, 0)) | ||
| 338 | + connection._execute(1, prepare) | ||
| 339 | + | ||
| 340 | + args, off = [], 1 # skip the function code, then walk [len(4)][value] args | ||
| 341 | + payload = sent[0] | ||
| 342 | + while off < len(payload): | ||
| 343 | + (length,) = struct.unpack(">i", payload[off:off + 4]) | ||
| 344 | + args.append(payload[off + 4:off + 4 + length]) | ||
| 345 | + off += 4 + length | ||
| 346 | + return bytearray(args[6])[0] # handle, flag, max_col_size, max_row, binds, fetch, auto_commit | ||
| 347 | + | ||
| 348 | + def test_dml_is_committed_by_the_execute_request(self): | ||
| 349 | + self.assertEqual(self._auto_commit_byte(_cubrid._STMT_SELECT + 1), 1) | ||
| 350 | + | ||
| 351 | + def test_select_does_not_end_the_transaction(self): | ||
| 352 | + self.assertEqual(self._auto_commit_byte(_cubrid._STMT_SELECT), 0) | ||
| 353 | + | ||
| 354 | + | ||
| 355 | + class BoundedReadTest(unittest.TestCase): | ||
| 356 | + """A length taken off the wire is attacker/corruption controlled: unchecked, it either reads until | ||
| 357 | + memory runs out or (on a short buffer) hands back silently truncated data.""" | ||
| 358 | + | ||
| 359 | + def test_cubrid_short_response_is_not_silently_truncated(self): | ||
| 360 | + reader = _cubrid._Reader(b"AB") | ||
| 361 | + self.assertRaises(dbwire.InterfaceError, reader.raw, 8) | ||
| 362 | + self.assertRaises(dbwire.InterfaceError, reader.raw, -1) | ||
| 363 | + | ||
| 364 | + def test_firebird_rejects_an_out_of_range_length(self): | ||
| 365 | + wire = _firebird._Wire(FakeSocket()) | ||
| 366 | + self.assertRaises(dbwire.InterfaceError, wire.recv, -1) | ||
| 367 | + self.assertRaises(dbwire.InterfaceError, wire.recv, _firebird._MAX_MESSAGE_LENGTH + 1) | ||
| 368 | + | ||
| 369 | + def test_monetdb_unterminated_block_stream_is_bounded(self): | ||
| 370 | + """The MAPI block length is a 15-bit field, so only the accumulated response can be bounded.""" | ||
| 371 | + | ||
| 372 | + block = struct.pack("<H", (4000 << 1) | 0) + b"A" * 4000 # last-flag clear -> never ends | ||
| 373 | + sock = FakeSocket(block * 32) | ||
| 374 | + original = sock.recv | ||
| 375 | + | ||
| 376 | + def recv(count): | ||
| 377 | + if not sock.inbound: | ||
| 378 | + sock.feed(block * 32) | ||
| 379 | + return original(count) | ||
| 380 | + | ||
| 381 | + sock.recv = recv | ||
| 382 | + saved = _monetdb._MAX_MESSAGE_LENGTH | ||
| 383 | + try: | ||
| 384 | + _monetdb._MAX_MESSAGE_LENGTH = 100000 | ||
| 385 | + self.assertRaises(dbwire.InterfaceError, _monetdb._getblock, sock) | ||
| 386 | + finally: | ||
| 387 | + _monetdb._MAX_MESSAGE_LENGTH = saved | ||
| 388 | + | ||
| 389 | + | ||
| 317 | 390 | class HelperTest(unittest.TestCase): | |
| 318 | 391 | def test_socket_failure_maps_into_the_dbapi_hierarchy(self): | |
| 319 | 392 | """Callers of a PEP 249 driver only catch Error and its subclasses.""" | |
| Back | FazBrowse Home | New Git URL |
0 commit comments