From e11ea65230cc09cc8f03f1a6fdafc85015b19e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 11 Aug 2026 23:13:20 +0200 Subject: [PATCH] Minor patch --- lib/controller/controller.py | 7 ++-- lib/core/option.py | 5 ++- lib/core/settings.py | 9 +++++- tests/test_option.py | 62 ++++++++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 7 deletions(-) diff --git a/lib/controller/controller.py b/lib/controller/controller.py index 66e08779e..b92fdb962 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -70,6 +70,7 @@ from lib.core.exception import SqlmapValueException from lib.core.settings import ASP_NET_CONTROL_REGEX from lib.core.settings import CSRF_TOKEN_PARAMETER_INFIXES from lib.core.settings import DEFAULT_GET_POST_DELIMITER +from lib.core.settings import NONSQL_TECHNIQUES from lib.core.settings import EMPTY_FORM_FIELDS_REGEX from lib.core.settings import GOOGLE_ANALYTICS_COOKIE_REGEX from lib.core.settings import HASHDB_STALE_DAYS @@ -532,12 +533,12 @@ def start(): checkJWT() - if conf.mineParams and not any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe, conf.xslt, conf.hql, conf.sparql, conf.odata, conf.jwt)): + if conf.mineParams and not any(conf.get(_) for _ in NONSQL_TECHNIQUES): from lib.utils.paraminer import mineParameters mineParameters() - if any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe, conf.xslt, conf.hql, conf.sparql, conf.odata, conf.jwt)) and (conf.reportJson or conf.resultsFile): - singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (--graphql/--nosql/--ldap/--xpath/--ssti/--xslt/--xxe/--hql/--sparql/--odata/--jwt) findings; these are reported on the console only") + if any(conf.get(_) for _ in NONSQL_TECHNIQUES) and (conf.reportJson or conf.resultsFile): + singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (%s) findings; these are reported on the console only" % '/'.join("--%s" % _ for _ in NONSQL_TECHNIQUES)) if conf.graphql: from lib.techniques.graphql.inject import graphqlScan diff --git a/lib/core/option.py b/lib/core/option.py index b778f416b..b6f98fb30 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -128,6 +128,7 @@ from lib.core.settings import PARAMETER_SPLITTING_REGEX from lib.core.settings import PRECONNECT_CANDIDATE_TIMEOUT from lib.core.settings import PROXY_ENVIRONMENT_VARIABLES from lib.core.settings import SOCKET_PRE_CONNECT_QUEUE_SIZE +from lib.core.settings import NONSQL_TECHNIQUES from lib.core.settings import SQLMAP_ENVIRONMENT_PREFIX from lib.core.settings import SUPPORTED_DBMS from lib.core.settings import SUPPORTED_OS @@ -2763,9 +2764,7 @@ def _checkTor(): logger.info(infoMsg) def _basicOptionValidation(): - _nonSqlTechniques = [name for name, enabled in ( - ("--graphql", conf.graphql), ("--nosql", conf.nosql), ("--ldap", conf.ldap), - ("--xpath", conf.xpath), ("--ssti", conf.ssti), ("--xxe", conf.xxe), ("--xslt", conf.xslt), ("--hql", conf.hql), ("--sparql", conf.sparql), ("--odata", conf.odata)) if enabled] + _nonSqlTechniques = ["--%s" % _ for _ in NONSQL_TECHNIQUES if conf.get(_)] if len(_nonSqlTechniques) > 1: errMsg = "only one non-SQL technique switch may be used at a time (found: %s). " % ", ".join(_nonSqlTechniques) errMsg += "each is a self-contained scan for a different back-end class - pick one" diff --git a/lib/core/settings.py b/lib/core/settings.py index ee4089ada..3cde5b8a9 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.8.26" +VERSION = "1.10.8.27" 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) @@ -1416,6 +1416,13 @@ HQL_ERROR_SIGNATURES = ( HQL_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in HQL_ERROR_SIGNATURES) +# The self-contained non-SQL technique switches, by conf option name (each is also its switch spelling, +# '--'). Each is a whole scan for a different back-end class, so at most one may run: the target +# loop branches on them and the option validation rejects a pair. Kept in ONE place because it used to +# be spelled out at every site and had already drifted - '--jwt' was missing from the validation, which +# let '--jwt --nosql' through to run neither (conf.jwt also suppresses the passive JWT heuristic) +NONSQL_TECHNIQUES = ("graphql", "nosql", "ldap", "xpath", "ssti", "xxe", "xslt", "hql", "sparql", "odata", "jwt") + # Small, fast dictionary the always-on JWT heuristic tries against an HS* signature (the full # '--jwt' audit streams the shipped wordlist instead); these are the secrets seen over and over in # tutorials, framework defaults and CTFs diff --git a/tests/test_option.py b/tests/test_option.py index 11f4d8bdf..1fe820107 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -19,6 +19,7 @@ shared state stays pristine for the other test files in the suite. import contextlib import logging import os +import re import socket import sys import tempfile @@ -28,13 +29,17 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _testutils import bootstrap bootstrap() +_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + from lib.core.data import conf, kb, logger from lib.core.common import Backend from lib.core.enums import AUTH_TYPE +from lib.core.optiondict import optDict from lib.core.enums import HTTP_HEADER from lib.core.settings import DEFAULT_USER_AGENT from lib.core.settings import IGNORE_CODE_WILDCARD from lib.core.settings import MAX_CONNECT_RETRIES +from lib.core.settings import NONSQL_TECHNIQUES from lib.core.exception import SqlmapFilePathException from lib.core.exception import SqlmapGenericException from lib.core.exception import SqlmapMissingMandatoryOptionException @@ -713,6 +718,37 @@ class TestBasicOptionValidation(_BackendGuard): self._base() option._basicOptionValidation() # must not raise + def test_one_non_sql_technique_at_a_time(self): + """Every pair must be refused, and refused BY NAME - assertRaises alone would be satisfied by + any unrelated validation error, which is how a missing entry hid here in the first place.""" + + with _preserve(conf, *(self._KEYS + NONSQL_TECHNIQUES)): + for name in NONSQL_TECHNIQUES: + for other in NONSQL_TECHNIQUES: + if other == name: + continue + self._base() + for _ in NONSQL_TECHNIQUES: + conf[_] = False + conf[name] = conf[other] = True + try: + option._basicOptionValidation() + except SqlmapSyntaxException as ex: + message = str(ex) + self.assertIn("--%s" % name, message) + self.assertIn("--%s" % other, message) + else: + self.fail("'--%s --%s' was accepted" % (name, other)) + + def test_single_non_sql_technique_passes(self): + with _preserve(conf, *(self._KEYS + NONSQL_TECHNIQUES)): + for name in NONSQL_TECHNIQUES: + self._base() + for _ in NONSQL_TECHNIQUES: + conf[_] = False + conf[name] = True + option._basicOptionValidation() # must not raise + def test_bad_level_raises(self): with _preserve(conf, *self._KEYS): self._base() @@ -1586,5 +1622,31 @@ class TestOptionSetAuthCred(unittest.TestCase): ) +class TestNonSqlTechniqueRegistry(unittest.TestCase): + """NONSQL_TECHNIQUES governs three things that used to be spelled out separately: the target loop's + branches, the '--mine-params'/'--report-json' gates, and the one-at-a-time validation. They had + already drifted - '--jwt' was in the branches but not the validation, so '--jwt --nosql' was + accepted and then ran NEITHER (the nosql branch wins, and conf.jwt suppresses the passive JWT + heuristic). Anchoring the list to the branches that consume it is what makes the next engine safe.""" + + def _read(self, *parts): + with open(os.path.join(_ROOT, *parts)) as f: + return f.read() + + def test_every_branch_is_registered(self): + branches = set(re.findall(r"from lib\.techniques\.(\w+)\.inject import", self._read("lib", "controller", "controller.py"))) + self.assertTrue(branches) + self.assertEqual(sorted(branches - set(NONSQL_TECHNIQUES)), []) + + def test_every_registered_technique_has_a_branch_and_a_switch(self): + controller = self._read("lib", "controller", "controller.py") + cmdline = self._read("lib", "parse", "cmdline.py") + + for name in NONSQL_TECHNIQUES: + self.assertIn("from lib.techniques.%s.inject import" % name, controller, name) + self.assertIn('dest="%s"' % name, cmdline, name) + self.assertEqual(optDict["Techniques"].get(name), "boolean", name) + + if __name__ == "__main__": unittest.main(verbosity=2)