Skip to content

Commit bda62dc

Browse files
committed
Fixes #6123
1 parent fa96906 commit bda62dc

5 files changed

Lines changed: 117 additions & 4 deletions

File tree

lib/controller/controller.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,11 @@ def start():
376376
for targetUrl, targetMethod, targetData, targetCookie, targetHeaders in kb.targets:
377377
targetCount += 1
378378

379+
# --report-json: give each target its own taskid in the shared collector, so a multi-target
380+
# run (e.g. '-m' bulk file) doesn't have later targets overwrite earlier ones (see _reportData)
381+
if conf.reportJson:
382+
kb.reportTaskId = targetCount
383+
379384
try:
380385
if conf.checkInternet:
381386
infoMsg = "checking for Internet connection"

lib/core/dump.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,15 @@ def _reportData(self, data, content_type):
108108
collector is active - which is only ever the case for a CLI --report-json run, never under
109109
--api - so this never double-captures alongside StdDbOut. A None content_type is resolved
110110
via the kb.partRun fallback (e.g. the fingerprint line), mirroring the API exactly.
111+
112+
Keyed by kb.reportTaskId rather than the fixed REPORT_TASKID so that a multi-target run
113+
(e.g. '-m' bulk file) keeps each target's results separate instead of later targets
114+
overwriting earlier ones under the same content_type.
111115
"""
112116

113117
if conf.get("reportCollector") is not None:
114118
from lib.utils.api import _storeData, REPORT_TASKID
115-
_storeData(conf.reportCollector, REPORT_TASKID, stdoutEncode(clearColors(data)), CONTENT_STATUS.COMPLETE, content_type)
119+
_storeData(conf.reportCollector, kb.get("reportTaskId", REPORT_TASKID), stdoutEncode(clearColors(data)), CONTENT_STATUS.COMPLETE, content_type)
116120

117121
def flush(self):
118122
if self._outputFP:

lib/core/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.9.12"
23+
VERSION = "1.10.9.13"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

lib/utils/api.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,19 @@ def writeReportJson(collector, filepath):
265265
"""
266266
Writes the collected results to filepath as JSON, in the same shape as the REST API's
267267
/scan/<id>/data response, wrapped with a small 'meta' block for standalone consumers.
268+
269+
A multi-target run (e.g. '-m' bulk file) stores each target under its own taskid (see
270+
kb.reportTaskId), so every taskid present in the collector is assembled here; a single-target
271+
run still yields exactly one taskid and keeps the flat {success, data, error} shape.
268272
"""
269273

270-
result = _assembleData(collector, REPORT_TASKID)
274+
taskids = sorted(row[0] for row in collector.execute("SELECT DISTINCT taskid FROM data")) or [REPORT_TASKID]
275+
276+
if len(taskids) > 1:
277+
result = {"success": True, "targets": [_assembleData(collector, taskid) for taskid in taskids]}
278+
else:
279+
result = _assembleData(collector, taskids[0])
280+
271281
result["meta"] = {
272282
"api_version": int(RESTAPI_VERSION.split(".")[0]), # MAJOR only - the part that matters for client compatibility
273283
"sqlmap_version": VERSION_STRING,
@@ -467,7 +477,8 @@ def __init__(self, collector):
467477

468478
def emit(self, record):
469479
try:
470-
self.collector.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (REPORT_TASKID, str(record.msg % record.args if record.args else record.msg)))
480+
taskid = kb.get("reportTaskId", REPORT_TASKID)
481+
self.collector.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (taskid, str(record.msg % record.args if record.args else record.msg)))
471482
except Exception:
472483
pass
473484

tests/test_report.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,5 +222,98 @@ def test_file_is_valid_json_with_meta(self):
222222
os.remove(path)
223223

224224

225+
class TestMultiTargetTaskId(_CollectorCase):
226+
"""
227+
Regression coverage for issue #6123: a '-m' bulk-file run shares ONE process (and thus one
228+
report collector) across many targets. _storeData()'s COMPLETE-status branch deletes any
229+
existing row for a (taskid, content_type) key before inserting the new one, so if every
230+
target reused the same fixed REPORT_TASKID, a later target's TARGET/TECHNIQUES write would
231+
silently delete an earlier target's row of the same content_type. The fix keys each target's
232+
writes by kb.reportTaskId instead.
233+
"""
234+
235+
def setUp(self):
236+
super(TestMultiTargetTaskId, self).setUp()
237+
from lib.core.dump import Dump
238+
self._saved_reportTaskId = kb.get("reportTaskId")
239+
self._saved_dumper = conf.get("dumper")
240+
self._saved_reportCollector = conf.get("reportCollector")
241+
conf.dumper = Dump()
242+
conf.reportCollector = self.c
243+
244+
def tearDown(self):
245+
kb.reportTaskId = self._saved_reportTaskId
246+
conf.dumper = self._saved_dumper
247+
conf.reportCollector = self._saved_reportCollector
248+
super(TestMultiTargetTaskId, self).tearDown()
249+
250+
def test_second_target_does_not_overwrite_first(self):
251+
kb.reportTaskId = 1
252+
conf.dumper._reportData({"url": "http://host1/?id=1"}, CONTENT_TYPE.TARGET)
253+
254+
kb.reportTaskId = 2
255+
conf.dumper._reportData({"url": "http://host2/?id=1"}, CONTENT_TYPE.TARGET)
256+
257+
first = api._assembleData(self.c, 1)["data"]
258+
second = api._assembleData(self.c, 2)["data"]
259+
self.assertEqual(first[0]["value"]["url"], "http://host1/?id=1") # not clobbered by target #2
260+
self.assertEqual(second[0]["value"]["url"], "http://host2/?id=1")
261+
262+
def test_write_report_json_wraps_multiple_targets(self):
263+
kb.reportTaskId = 1
264+
conf.dumper._reportData({"url": "http://host1/?id=1"}, CONTENT_TYPE.TARGET)
265+
kb.reportTaskId = 2
266+
conf.dumper._reportData({"url": "http://host2/?id=1"}, CONTENT_TYPE.TARGET)
267+
268+
fd, path = tempfile.mkstemp(suffix=".json")
269+
os.close(fd)
270+
try:
271+
api.writeReportJson(self.c, path)
272+
with io.open(path, encoding="utf-8") as f:
273+
loaded = json.load(f)
274+
self.assertIn("targets", loaded)
275+
self.assertEqual(len(loaded["targets"]), 2)
276+
urls = [t["data"][0]["value"]["url"] for t in loaded["targets"]]
277+
self.assertEqual(urls, ["http://host1/?id=1", "http://host2/?id=1"])
278+
finally:
279+
os.remove(path)
280+
281+
def test_single_target_report_keeps_flat_shape(self):
282+
# backward compatibility: exactly one taskid -> no 'targets' wrapper, same shape as before
283+
kb.reportTaskId = 1
284+
conf.dumper._reportData({"url": "http://host1/?id=1"}, CONTENT_TYPE.TARGET)
285+
286+
fd, path = tempfile.mkstemp(suffix=".json")
287+
os.close(fd)
288+
try:
289+
api.writeReportJson(self.c, path)
290+
with io.open(path, encoding="utf-8") as f:
291+
loaded = json.load(f)
292+
self.assertNotIn("targets", loaded)
293+
self.assertEqual(loaded["data"][0]["value"]["url"], "http://host1/?id=1")
294+
finally:
295+
os.remove(path)
296+
297+
def test_error_recorded_under_active_target(self):
298+
import logging
299+
from lib.core.data import logger
300+
301+
saved_level = logger.level
302+
logger.setLevel(logging.ERROR)
303+
# mute pre-existing handlers (e.g. console) but not the ReportErrorRecorder added by setUp
304+
muted = [(handler, handler.level) for handler in logger.handlers if not isinstance(handler, api.ReportErrorRecorder)]
305+
for handler, _ in muted:
306+
handler.setLevel(logging.CRITICAL + 1)
307+
try:
308+
kb.reportTaskId = 2
309+
logger.error("boom for target 2")
310+
self.assertTrue(any("boom for target 2" in _ for _ in api._assembleData(self.c, 2)["error"]))
311+
self.assertEqual(api._assembleData(self.c, 1)["error"], []) # not attributed to target #1
312+
finally:
313+
logger.setLevel(saved_level)
314+
for handler, level in muted:
315+
handler.setLevel(level)
316+
317+
225318
if __name__ == "__main__":
226319
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)