This commit is contained in:
Miroslav Štampar
2026-09-18 00:41:57 +02:00
parent fa96906f76
commit bda62dcf28
5 changed files with 117 additions and 4 deletions
+5
View File
@@ -376,6 +376,11 @@ def start():
for targetUrl, targetMethod, targetData, targetCookie, targetHeaders in kb.targets:
targetCount += 1
# --report-json: give each target its own taskid in the shared collector, so a multi-target
# run (e.g. '-m' bulk file) doesn't have later targets overwrite earlier ones (see _reportData)
if conf.reportJson:
kb.reportTaskId = targetCount
try:
if conf.checkInternet:
infoMsg = "checking for Internet connection"
+5 -1
View File
@@ -108,11 +108,15 @@ class Dump(object):
collector is active - which is only ever the case for a CLI --report-json run, never under
--api - so this never double-captures alongside StdDbOut. A None content_type is resolved
via the kb.partRun fallback (e.g. the fingerprint line), mirroring the API exactly.
Keyed by kb.reportTaskId rather than the fixed REPORT_TASKID so that a multi-target run
(e.g. '-m' bulk file) keeps each target's results separate instead of later targets
overwriting earlier ones under the same content_type.
"""
if conf.get("reportCollector") is not None:
from lib.utils.api import _storeData, REPORT_TASKID
_storeData(conf.reportCollector, REPORT_TASKID, stdoutEncode(clearColors(data)), CONTENT_STATUS.COMPLETE, content_type)
_storeData(conf.reportCollector, kb.get("reportTaskId", REPORT_TASKID), stdoutEncode(clearColors(data)), CONTENT_STATUS.COMPLETE, content_type)
def flush(self):
if self._outputFP:
+1 -1
View File
@@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.9.12"
VERSION = "1.10.9.13"
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)
+13 -2
View File
@@ -265,9 +265,19 @@ def writeReportJson(collector, filepath):
"""
Writes the collected results to filepath as JSON, in the same shape as the REST API's
/scan/<id>/data response, wrapped with a small 'meta' block for standalone consumers.
A multi-target run (e.g. '-m' bulk file) stores each target under its own taskid (see
kb.reportTaskId), so every taskid present in the collector is assembled here; a single-target
run still yields exactly one taskid and keeps the flat {success, data, error} shape.
"""
result = _assembleData(collector, REPORT_TASKID)
taskids = sorted(row[0] for row in collector.execute("SELECT DISTINCT taskid FROM data")) or [REPORT_TASKID]
if len(taskids) > 1:
result = {"success": True, "targets": [_assembleData(collector, taskid) for taskid in taskids]}
else:
result = _assembleData(collector, taskids[0])
result["meta"] = {
"api_version": int(RESTAPI_VERSION.split(".")[0]), # MAJOR only - the part that matters for client compatibility
"sqlmap_version": VERSION_STRING,
@@ -467,7 +477,8 @@ class ReportErrorRecorder(logging.Handler):
def emit(self, record):
try:
self.collector.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (REPORT_TASKID, str(record.msg % record.args if record.args else record.msg)))
taskid = kb.get("reportTaskId", REPORT_TASKID)
self.collector.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (taskid, str(record.msg % record.args if record.args else record.msg)))
except Exception:
pass
+93
View File
@@ -222,5 +222,98 @@ class TestWriteReportJson(_CollectorCase):
os.remove(path)
class TestMultiTargetTaskId(_CollectorCase):
"""
Regression coverage for issue #6123: a '-m' bulk-file run shares ONE process (and thus one
report collector) across many targets. _storeData()'s COMPLETE-status branch deletes any
existing row for a (taskid, content_type) key before inserting the new one, so if every
target reused the same fixed REPORT_TASKID, a later target's TARGET/TECHNIQUES write would
silently delete an earlier target's row of the same content_type. The fix keys each target's
writes by kb.reportTaskId instead.
"""
def setUp(self):
super(TestMultiTargetTaskId, self).setUp()
from lib.core.dump import Dump
self._saved_reportTaskId = kb.get("reportTaskId")
self._saved_dumper = conf.get("dumper")
self._saved_reportCollector = conf.get("reportCollector")
conf.dumper = Dump()
conf.reportCollector = self.c
def tearDown(self):
kb.reportTaskId = self._saved_reportTaskId
conf.dumper = self._saved_dumper
conf.reportCollector = self._saved_reportCollector
super(TestMultiTargetTaskId, self).tearDown()
def test_second_target_does_not_overwrite_first(self):
kb.reportTaskId = 1
conf.dumper._reportData({"url": "http://host1/?id=1"}, CONTENT_TYPE.TARGET)
kb.reportTaskId = 2
conf.dumper._reportData({"url": "http://host2/?id=1"}, CONTENT_TYPE.TARGET)
first = api._assembleData(self.c, 1)["data"]
second = api._assembleData(self.c, 2)["data"]
self.assertEqual(first[0]["value"]["url"], "http://host1/?id=1") # not clobbered by target #2
self.assertEqual(second[0]["value"]["url"], "http://host2/?id=1")
def test_write_report_json_wraps_multiple_targets(self):
kb.reportTaskId = 1
conf.dumper._reportData({"url": "http://host1/?id=1"}, CONTENT_TYPE.TARGET)
kb.reportTaskId = 2
conf.dumper._reportData({"url": "http://host2/?id=1"}, CONTENT_TYPE.TARGET)
fd, path = tempfile.mkstemp(suffix=".json")
os.close(fd)
try:
api.writeReportJson(self.c, path)
with io.open(path, encoding="utf-8") as f:
loaded = json.load(f)
self.assertIn("targets", loaded)
self.assertEqual(len(loaded["targets"]), 2)
urls = [t["data"][0]["value"]["url"] for t in loaded["targets"]]
self.assertEqual(urls, ["http://host1/?id=1", "http://host2/?id=1"])
finally:
os.remove(path)
def test_single_target_report_keeps_flat_shape(self):
# backward compatibility: exactly one taskid -> no 'targets' wrapper, same shape as before
kb.reportTaskId = 1
conf.dumper._reportData({"url": "http://host1/?id=1"}, CONTENT_TYPE.TARGET)
fd, path = tempfile.mkstemp(suffix=".json")
os.close(fd)
try:
api.writeReportJson(self.c, path)
with io.open(path, encoding="utf-8") as f:
loaded = json.load(f)
self.assertNotIn("targets", loaded)
self.assertEqual(loaded["data"][0]["value"]["url"], "http://host1/?id=1")
finally:
os.remove(path)
def test_error_recorded_under_active_target(self):
import logging
from lib.core.data import logger
saved_level = logger.level
logger.setLevel(logging.ERROR)
# mute pre-existing handlers (e.g. console) but not the ReportErrorRecorder added by setUp
muted = [(handler, handler.level) for handler in logger.handlers if not isinstance(handler, api.ReportErrorRecorder)]
for handler, _ in muted:
handler.setLevel(logging.CRITICAL + 1)
try:
kb.reportTaskId = 2
logger.error("boom for target 2")
self.assertTrue(any("boom for target 2" in _ for _ in api._assembleData(self.c, 2)["error"]))
self.assertEqual(api._assembleData(self.c, 1)["error"], []) # not attributed to target #1
finally:
logger.setLevel(saved_level)
for handler, level in muted:
handler.setLevel(level)
if __name__ == "__main__":
unittest.main(verbosity=2)