This commit is contained in:
Miroslav Štampar
2026-10-19 11:46:27 +02:00
parent e90078184d
commit 6530184e2c
14 changed files with 545 additions and 11 deletions
+1
View File
@@ -463,6 +463,7 @@ class MKSTEMP_PREFIX(object):
BIG_ARRAY = "sqlmapbigarray-"
SPECIFIC_RESPONSE = "sqlmapresponse-"
PREPROCESS = "sqlmappreprocess-"
AUTH_REFRESH = "sqlmapauthrefresh-"
class TIMEOUT_STATE(object):
NORMAL = 0
+44 -1
View File
@@ -142,6 +142,7 @@ from lib.parse.configfile import configFileParser
from lib.parse.payloads import loadBoundaries
from lib.parse.payloads import loadPayloads
from lib.request.basic import checkCharEncoding
from lib.request import authrefresh
from lib.request.basicauthhandler import SmartHTTPBasicAuthHandler
from lib.request.chunkedhandler import ChunkedHandler
from lib.request.connect import Connect as Request
@@ -1152,6 +1153,42 @@ def _setPostprocessFunctions():
errMsg += "(Note: find template script at '%s')" % filename
raise SqlmapGenericException(errMsg)
def _setAuthRefreshFunction():
"""
Load the authentication refresh hook without performing a login during initialization.
"""
if conf.authRefresh:
try:
script = safeFilepathEncode(conf.authRefresh.strip())
if not os.path.isfile(script):
raise SqlmapFilePathException("auth refresh script '%s' does not exist" % script)
if not script.endswith(".py"):
raise SqlmapSyntaxException("auth refresh script should have an extension '.py'")
dirname, filename = os.path.split(os.path.abspath(script))
if dirname not in sys.path:
sys.path.insert(0, dirname)
logger.info("loading auth refresh module '%s'" % filename[:-3])
try:
getattr(importlib, "invalidate_caches", lambda: None)()
module = __import__(safeFilepathEncode(filename[:-3]))
function = getattr(module, "refresh", None)
if not inspect.isfunction(function):
raise ValueError("missing function 'refresh()'")
spec = _argSpec(function)
if len(spec.args) > len(spec.defaults or ()) or any(_ not in (getattr(spec, "kwonlydefaults", None) or {}) for _ in getattr(spec, "kwonlyargs", ())):
raise ValueError("function 'refresh()' must be callable without arguments")
except Exception:
raise SqlmapSyntaxException("unable to load 'refresh()' from auth refresh script '%s'" % script)
except (SqlmapFilePathException, SqlmapSyntaxException) as ex:
errMsg = "%s. You can find a template script at '%s'" % (getSafeExString(ex), authrefresh.createTemplate())
raise type(ex)(errMsg)
kb.authRefreshFunction = function
def _setThreads():
if not isinstance(conf.threads, int) or conf.threads <= 0:
conf.threads = 1
@@ -2317,9 +2354,13 @@ def _setKnowledgeBaseAttributes(flushAll=True):
kb.lastParserStatus = None
kb.locks = AttribDict()
for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "multibit", "prediction", "socket", "redirect", "request", "value"):
for _ in ("authRefresh", "cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "multibit", "prediction", "socket", "redirect", "request", "value"):
kb.locks[_] = threading.Lock()
kb.authRefreshHeaders = {}
kb.authRefreshVersion = 0
kb.authRefreshFailed = False
kb.matchRatio = None
kb.maxConnectionsFlag = False
kb.trueLength = None
@@ -2427,6 +2468,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
kb.passwordMgr = None
kb.postprocessFunctions = []
kb.preprocessFunctions = []
kb.authRefreshFunction = None
kb.skipVulnHost = None
kb.storeCrawlingChoice = None
kb.tamperFunctions = []
@@ -3171,6 +3213,7 @@ def init():
_setTamperingFunctions()
_setPreprocessFunctions()
_setPostprocessFunctions()
_setAuthRefreshFunction()
_setTrafficOutputFP()
_setupHTTPCollector()
_setHttpOptions()
+2 -1
View File
@@ -257,8 +257,9 @@ optDict = {
"hexConvert": "boolean",
"outputDir": "string",
"parseErrors": "boolean",
"postprocess": "string",
"authRefresh": "string",
"preprocess": "string",
"postprocess": "string",
"repair": "boolean",
"reportJson": "string",
"saveConfig": "string",
+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.10.9"
VERSION = "1.10.10.11"
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)
+1
View File
@@ -60,6 +60,7 @@ class _ThreadData(threading.local):
self.lastHTTPError = None
self.lastRedirectMsg = None
self.lastQueryDuration = 0
self.authRefreshTime = 0
self.lastPage = None
self.lastRequestMsg = None
self.lastRequestUID = 0
+3
View File
@@ -748,6 +748,9 @@ def cmdLineParser(argv=None):
general.add_argument("--parse-errors", dest="parseErrors", action="store_true",
help="Parse and display DBMS error messages from responses")
general.add_argument("--auth-refresh", dest="authRefresh",
help="Run script to refresh auth on HTTP 401")
general.add_argument("--preprocess", dest="preprocess",
help="Use given script(s) for preprocessing (request)")
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import os
import re
import tempfile
from lib.core.common import openFile
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger
from lib.core.enums import MKSTEMP_PREFIX
from lib.core.exception import SqlmapConnectionException
from lib.core.exception import SqlmapGenericException
from thirdparty import six
from thirdparty.six.moves import urllib as _urllib
def createTemplate():
handle, filename = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.AUTH_REFRESH, suffix=".py")
os.close(handle)
with openFile(filename, "w+") as f:
f.write("#!/usr/bin/env\n\ndef refresh():\n # Obtain a fresh token here (use a timeout for login requests).\n # Custom headers also work, e.g. {'X-Auth-Token': token}.\n return {'Authorization': 'Bearer <fresh token>'}\n")
with openFile(os.path.join(os.path.dirname(filename), "__init__.py"), "w+") as f:
f.write("pass")
return filename
def sameOrigin(first, second):
# checkSameHost deliberately ignores schemes and ports, which is unsuitable for credentials.
def origin(url):
parts = _urllib.parse.urlsplit(url)
return parts.scheme.lower(), parts.hostname, parts.port or (443 if parts.scheme.lower() == "https" else 80)
return origin(first) == origin(second)
def apply(req):
"""Overlay refreshed headers after preprocess hooks; return the credential generation sent."""
if not conf.authRefresh or not sameOrigin(req.get_full_url(), conf.url):
return None
with kb.locks.authRefresh:
if kb.authRefreshFailed:
raise SqlmapGenericException("authentication refresh failed; cannot continue with expired credentials")
for name, value in kb.authRefreshHeaders.items():
# Hooks/raw request files can leave differently-cased copies of the same header.
for headers in (req.headers, req.unredirected_hdrs):
for key in list(headers):
if key.lower() == name.lower():
del headers[key]
req.add_header(name, value)
return kb.authRefreshVersion
def refresh(version):
"""Refresh once for all workers that sent the same expired credentials."""
with kb.locks.authRefresh:
if kb.authRefreshFailed:
raise SqlmapConnectionException("authentication refresh failed; cannot continue with expired credentials")
if version != kb.authRefreshVersion:
return
logger.info("refreshing authentication after HTTP 401")
try:
headers = kb.authRefreshFunction()
if not isinstance(headers, dict) or not headers:
raise ValueError()
for name, value in headers.items():
if not isinstance(name, six.string_types) or not re.match(r"\A[!#$%&'*+.^_`|~0-9A-Za-z-]+\Z", name):
raise ValueError()
if not isinstance(value, six.string_types) or not value or re.search(r"[\x00-\x1f\x7f]", value):
raise ValueError()
value.encode("latin-1")
if name.lower() in ("host", "content-length", "transfer-encoding", "connection", "proxy-authorization"):
raise ValueError()
if len(set(_.lower() for _ in headers)) != len(headers):
raise ValueError()
except Exception:
kb.authRefreshFailed = True
# Hook exceptions can contain tokens/passwords; do not include their text.
raise SqlmapConnectionException("auth refresh hook failed or returned invalid headers. Expected a non-empty dictionary of authentication headers. You can find a template script at '%s'" % createTemplate())
kb.authRefreshHeaders = dict(headers)
kb.authRefreshVersion += 1
def stripHeaders(headers):
"""Remove refreshed credentials before following a redirect to another origin."""
names = set(_.lower() for _ in kb.authRefreshHeaders)
return dict((name, value) for name, value in headers.items() if name.lower() not in names)
+29 -6
View File
@@ -22,6 +22,7 @@ import time
import traceback
from lib.request import websocket
from lib.request import authrefresh
from lib.request.websocket import WebSocketException
from lib.core.agent import agent
@@ -469,6 +470,8 @@ class Connect(object):
responseHeaders = None
logHeaders = u""
skipLogTraffic = False
authRetry = False
authVersion = None
raise404 = raise404 and not kb.ignoreNotFound
@@ -683,6 +686,9 @@ class Connect(object):
else:
post, headers = req.data, req.headers
authVersion = authrefresh.apply(req)
headers = req.headers
requestHeaders += "\r\n".join(["%s: %s" % (u"-".join(_.capitalize() for _ in getUnicode(key).split(u'-')) if hasattr(key, "capitalize") else getUnicode(key), getUnicode(value)) for (key, value) in req.header_items()])
if not getRequestHeader(req, HTTP_HEADER.COOKIE) and conf.cj:
@@ -730,12 +736,12 @@ class Connect(object):
raise SqlmapMissingDependence("native HTTP/2 client does not support SOCKS proxies (omit '--http2' or use an HTTP proxy)")
try:
conn = http2OpenUrl(url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post, timeout, follow_redirects=kb.choices.redirect != REDIRECTION.NO, proxy=h2proxy)
conn = http2OpenUrl(url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post, timeout, follow_redirects=kb.choices.redirect != REDIRECTION.NO, proxy=h2proxy, sensitive_headers=tuple(kb.authRefreshHeaders) if conf.authRefresh else ())
except IOError as ex:
raise _http_client.HTTPException(getSafeExString(ex))
else:
if conn.code >= 400:
raise _urllib.error.HTTPError(url, conn.code, conn.msg, conn.info(), io.BytesIO(conn.read()))
raise _urllib.error.HTTPError(conn.geturl(), conn.code, conn.msg, conn.info(), io.BytesIO(conn.read()))
requestMsg = re.sub(r" HTTP/[0-9.]+\r\n", " %s\r\n" % conn.http_version, requestMsg, count=1)
@@ -863,7 +869,7 @@ class Connect(object):
page = None
responseHeaders = None
if checking:
if checking and not (ex.code == _http_client.UNAUTHORIZED and authVersion is not None):
return None, None, None
try:
@@ -887,7 +893,8 @@ class Connect(object):
code = ex.code
status = getUnicode(getattr(ex, "reason", None) or getSafeExString(ex).split(": ", 1)[-1])
kb.originalCode = kb.originalCode or code
if not (code == _http_client.UNAUTHORIZED and authVersion is not None):
kb.originalCode = kb.originalCode or code
threadData.lastHTTPError = (threadData.lastRequestUID, code, status)
kb.httpErrorCodes[code] = kb.httpErrorCodes.get(code, 0) + 1
@@ -913,7 +920,15 @@ class Connect(object):
singleTimeLogMessage(errMsg, logging.CRITICAL)
raise SystemExit
if ex.code not in (conf.ignoreCode or []):
if ex.code == _http_client.UNAUTHORIZED and authVersion is not None and authrefresh.sameOrigin(ex.geturl(), url):
if kwargs.get("authRetried"):
with kb.locks.authRefresh:
if authVersion == kb.authRefreshVersion:
kb.authRefreshFailed = True
raise SqlmapConnectionException("authentication refresh did not authorize the retried request (401)")
authrefresh.refresh(authVersion)
authRetry = True
elif ex.code not in (conf.ignoreCode or []):
if ex.code == TOO_MANY_REQUESTS_HTTP_CODE or (ex.code == _http_client.SERVICE_UNAVAILABLE and Connect._parseRetryAfter(responseHeaders) is not None):
retried = Connect._rateLimitRetry(responseHeaders, ex.code, **kwargs)
if retried is not None:
@@ -1099,6 +1114,12 @@ class Connect(object):
socket.setdefaulttimeout(conf.timeout)
if authRetry:
# Authentication latency is not evidence of a time-based SQL injection.
threadData.authRefreshTime += calculateDeltaSeconds(start)
kwargs["authRetried"] = True
return Connect.getPage(**kwargs)
# Dirty patch for Python3.11.0a7 (e.g. https://github.com/sqlmapproject/sqlmap/issues/5091)
if not sys.version.startswith("3.11."):
if conf.retryOn and re.search(conf.retryOn, page or "", re.I):
@@ -1751,6 +1772,8 @@ class Connect(object):
start = time.time()
authRefreshTime = threadData.authRefreshTime
if kb.nullConnection and not content and not response and not timeBasedCompare:
noteResponseTime = False
@@ -1806,7 +1829,7 @@ class Connect(object):
return value
page, headers, code = Connect.getPage(url=_(kb.secondReq[0]), post=_(kb.secondReq[2]), method=kb.secondReq[1], cookie=kb.secondReq[3], silent=silent, auxHeaders=dict(auxHeaders, **dict(kb.secondReq[4])), response=response, raise404=False, ignoreTimeout=timeBasedCompare, refreshing=True)
threadData.lastQueryDuration = calculateDeltaSeconds(start)
threadData.lastQueryDuration = max(0, calculateDeltaSeconds(start) - (threadData.authRefreshTime - authRefreshTime))
kb.originalCode = code if kb.originalCode is None else kb.originalCode
kb.originalPage = page if kb.originalPage is None else kb.originalPage
+2 -2
View File
@@ -1576,7 +1576,7 @@ def proxy_tuple(proxy, cred=None):
def open_url(url, method="GET", headers=None, body=None, timeout=30, follow_redirects=True,
max_redirects=10, proxy=None, verify=False, ssl_context=None):
max_redirects=10, proxy=None, verify=False, ssl_context=None, sensitive_headers=()):
req_headers = headers or {}
current_method = method
current_body = body
@@ -1614,7 +1614,7 @@ def open_url(url, method="GET", headers=None, body=None, timeout=30, follow_redi
old_parts = parts
new_parts = urlsplit(new_url)
if _origin_tuple(old_parts) != _origin_tuple(new_parts):
req_headers = _headers_without(req_headers, _SENSITIVE_REDIRECT_FIELDS)
req_headers = _headers_without(req_headers, _SENSITIVE_REDIRECT_FIELDS.union(_as_bytes(_).lower() for _ in sensitive_headers))
method_bytes = _as_bytes(current_method)
change_to_get = status == 303 and method_bytes != b"HEAD"
+4
View File
@@ -32,6 +32,7 @@ from lib.core.settings import MAX_TOTAL_REDIRECTIONS
from lib.core.threads import getCurrentThreadData
from lib.request.basic import decodePage
from lib.request.basic import parseResponse
from lib.request import authrefresh
from thirdparty import six
from thirdparty.six.moves import http_client as _http_client
from thirdparty.six.moves import urllib as _urllib
@@ -176,6 +177,9 @@ class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler):
req.headers[HTTP_HEADER.COOKIE] = delimiter.join("%s=%s" % (key, cookies[key]) for key in cookies)
try:
if conf.authRefresh and not authrefresh.sameOrigin(req.get_full_url(), redurl):
req.headers = authrefresh.stripHeaders(req.headers)
req.unredirected_hdrs = authrefresh.stripHeaders(req.unredirected_hdrs)
result = _urllib.request.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
except _urllib.error.HTTPError as ex:
result = error = ex
+3
View File
@@ -809,6 +809,9 @@ outputDir =
# Valid: True or False
parseErrors = False
# Run a Python refresh() script returning replacement headers on HTTP 401.
authRefresh =
# Use given script(s) for preprocessing of request.
preprocess =
+341
View File
@@ -0,0 +1,341 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Authentication refresh hook loading and replay against a local HTTP server.
"""
import json
import os
import re
import shutil
import socket
import subprocess
import sys
import tempfile
import threading
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.data import conf, kb, paths, cmdLineOptions
from lib.core.enums import MKSTEMP_PREFIX, REDIRECTION
from lib.core.exception import SqlmapConnectionException, SqlmapFilePathException, SqlmapSyntaxException
from lib.core.option import _setAuthRefreshFunction, _setKnowledgeBaseAttributes
from lib.core.threads import getCurrentThreadData
from lib.request import authrefresh
from lib.request.connect import Connect
from lib.request.redirecthandler import SmartRedirectHandler
from thirdparty.six.moves import BaseHTTPServer, socketserver
from thirdparty.six.moves import urllib as _urllib
_GET_PAGE = Connect.__dict__['getPage']
_QUERY_PAGE = Connect.__dict__['queryPage']
def _assertTemplate(testCase, exception):
match = re.search(r"template script at '([^']+)'", str(exception))
testCase.assertIsNotNone(match)
path = match.group(1)
try:
testCase.assertTrue(os.path.basename(path).startswith(MKSTEMP_PREFIX.AUTH_REFRESH))
testCase.assertTrue(os.path.isfile(os.path.join(os.path.dirname(path), '__init__.py')))
with open(path) as stream:
source = stream.read()
namespace = {}
eval(compile(source, path, 'exec'), namespace)
testCase.assertEqual(namespace['refresh'](), {'Authorization': 'Bearer <fresh token>'})
testCase.assertNotIn(paths.SQLMAP_EXTRAS_PATH, path)
finally:
os.unlink(path)
class _Server(socketserver.ThreadingMixIn, BaseHTTPServer.HTTPServer):
daemon_threads = True
class _Handler(BaseHTTPServer.BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def do_GET(self):
self.do_POST()
def do_POST(self):
body = self.rfile.read(int(self.headers.get('Content-Length', '0')))
token = self.headers.get('X-Auth-Token')
if self.path == '/refresh':
self.server.refreshes += 1
code, body = 200, b'{"token":"fresh"}'
else:
with self.server.lock:
self.server.requests.append((self.path, body, token))
if token != self.server.token:
self.server.expired += 1
if self.server.expired >= 2:
self.server.bothExpired.set()
if token != self.server.token and self.server.syncExpired:
self.server.bothExpired.wait(3)
code = 200 if token == self.server.token else 401
body = b'ok' if code == 200 else b'expired'
self.send_response(302 if self.path == '/redirect' and code == 200 else code)
if self.path == '/redirect' and code == 200:
self.send_header('Location', self.server.redirect)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
class TestAuthRefresh(unittest.TestCase):
def setUp(self):
self.conf = dict(conf)
self.cmdLineOptions = dict(cmdLineOptions)
cmdLineOptions.method = None
self.kb = dict(kb)
self.threadData = dict(getCurrentThreadData().__dict__)
getCurrentThreadData().__dict__.clear()
getCurrentThreadData().reset()
self.timeout = socket.getdefaulttimeout()
self.urlopen = _urllib.request.urlopen
# Other engine tests replace these descriptors; use the real methods even when an
# earlier Python 2 fixture restored a bare function instead of a staticmethod.
self.methods = Connect.__dict__['getPage'], Connect.__dict__['queryPage']
Connect.getPage, Connect.queryPage = _GET_PAGE, _QUERY_PAGE
_setKnowledgeBaseAttributes(False)
kb.wizardMode = True
self.server = _Server(('127.0.0.1', 0), _Handler)
self.server.requests = []
self.server.refreshes = 0
self.server.token = 'fresh'
self.server.expired = 0
self.server.syncExpired = False
self.server.bothExpired = threading.Event()
self.server.lock = threading.Lock()
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.daemon = True
self.thread.start()
conf.url = 'http://127.0.0.1:%d/test' % self.server.server_port
conf.hostname, conf.scheme, conf.port = '127.0.0.1', 'http', self.server.server_port
conf.path = '/test'
conf.httpHeaders = [('X-Auth-Token', 'expired')]
conf.parameters = {}
conf.data = None
conf.abortCode = []
conf.ignoreCode = []
conf.authRefresh = 'test_refresh.py'
conf.batch = True
conf.keepAlive = False
conf.retryOn = None
conf.timeout = 5
conf.ignoreRedirects = False
conf.scope = None
kb.choices.redirect = REDIRECTION.YES
_urllib.request.urlopen = _urllib.request.build_opener(_urllib.request.ProxyHandler({}), SmartRedirectHandler()).open
def refresh():
# A real, application-specific token exchange outside sqlmap's request engine.
response = _urllib.request.urlopen(conf.url.replace('/test', '/refresh'), timeout=3)
try:
return {'X-Auth-Token': json.loads(response.read().decode('ascii'))['token']}
finally:
response.close()
kb.authRefreshFunction = refresh
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join(5)
_urllib.request.urlopen = self.urlopen
Connect.getPage, Connect.queryPage = self.methods
socket.setdefaulttimeout(self.timeout)
conf.clear()
conf.update(self.conf)
cmdLineOptions.clear()
cmdLineOptions.update(self.cmdLineOptions)
kb.clear()
kb.update(self.kb)
getCurrentThreadData().__dict__.clear()
getCurrentThreadData().__dict__.update(self.threadData)
def test_expiry_mid_scan_preserves_payload_and_reuses_headers(self):
self.server.token = 'expired'
self.assertEqual(Connect.getPage()[2], 200)
self.assertEqual(self.server.refreshes, 0)
self.server.token = 'fresh'
payload = b'id=1%20AND%201%3D1&other=keep'
self.assertEqual(Connect.getPage(get='marker=123', post=payload)[2], 200)
self.assertEqual(self.server.requests[-2:], [('/test?marker=123', payload, 'expired'), ('/test?marker=123', payload, 'fresh')])
self.assertEqual(Connect.getPage()[2], 200)
self.assertEqual(self.server.refreshes, 1)
self.assertEqual(getCurrentThreadData().lastCode, 200)
self.assertEqual(getCurrentThreadData().lastPage, 'ok')
self.assertNotEqual(kb.originalCode, 401)
def test_second_401_stops_after_one_replay(self):
kb.authRefreshFunction = lambda: {'X-Auth-Token': 'still-expired'}
with self.assertRaises(SqlmapConnectionException):
Connect.getPage()
self.assertEqual(len(self.server.requests), 2)
self.assertTrue(kb.authRefreshFailed)
def test_abort_code_takes_precedence(self):
conf.abortCode = [401]
with self.assertRaises(SystemExit):
Connect.getPage()
self.assertEqual(self.server.refreshes, 0)
self.assertEqual(len(self.server.requests), 1)
def test_without_option_preserves_401_behavior(self):
conf.authRefresh = None
with self.assertRaises(SqlmapConnectionException):
Connect.getPage()
self.assertEqual(self.server.refreshes, 0)
def test_concurrent_401s_share_one_refresh(self):
self.server.syncExpired = True
results, errors = [], []
def worker():
try:
results.append(Connect.getPage()[2])
except Exception as ex:
errors.append(ex)
workers = [threading.Thread(target=worker) for _ in range(2)]
for workerThread in workers:
workerThread.start()
for workerThread in workers:
workerThread.join(10)
self.assertFalse(workerThread.is_alive())
self.assertEqual(errors, [])
self.assertEqual(results, [200, 200])
self.assertEqual(self.server.refreshes, 1)
self.assertEqual(self.server.expired, 2)
def test_invalid_hook_results_are_rejected_without_leaking_secrets(self):
for result in (None, {}, 'secret-token', {'X-Token': 'secret\r\nInjected: value'}, {'Host': 'elsewhere'}, {'X-Token': 42}, {'X-Token': 'secret', 'x-token': 'other'}):
kb.authRefreshFailed = False
kb.authRefreshFunction = lambda: result
with self.assertRaises(SqlmapConnectionException) as caught:
authrefresh.refresh(0)
self.assertNotIn('secret', str(caught.exception))
_assertTemplate(self, caught.exception)
self.assertEqual(kb.authRefreshVersion, 0)
def test_hook_exception_is_sanitized(self):
def refresh():
raise ValueError('password=secret')
kb.authRefreshFunction = refresh
with self.assertRaises(SqlmapConnectionException) as caught:
Connect.getPage()
self.assertNotIn('secret', str(caught.exception))
_assertTemplate(self, caught.exception)
self.assertEqual(len(self.server.requests), 1)
def test_credentials_are_scoped_to_scheme_host_and_port(self):
authrefresh.refresh(0)
for url in ('http://elsewhere/test', 'https://127.0.0.1/test', 'http://127.0.0.1:1/test'):
req = _urllib.request.Request(url)
self.assertIsNone(authrefresh.apply(req))
self.assertEqual(req.headers, {})
self.assertTrue(authrefresh.sameOrigin('https://example.com', 'https://example.com:443/path'))
def test_refresh_overrides_stale_preprocess_headers(self):
kb.preprocessFunctions = [lambda req: req.add_header('X-Auth-Token', 'expired')]
self.assertEqual(Connect.getPage()[2], 200)
self.assertEqual(self.server.refreshes, 1)
def test_redirect_to_another_port_does_not_receive_refreshed_token(self):
authrefresh.refresh(0)
other = _Server(('127.0.0.1', 0), _Handler)
other.requests, other.token, other.expired = [], None, 0
other.lock = threading.Lock()
other.syncExpired = False
thread = threading.Thread(target=other.serve_forever)
thread.daemon = True
thread.start()
try:
self.server.redirect = 'http://127.0.0.1:%d/landing' % other.server_port
Connect.getPage(url=conf.url.replace('/test', '/redirect'))
self.assertEqual(other.requests, [('/landing', b'', None)])
self.assertEqual(self.server.refreshes, 1)
finally:
other.shutdown()
other.server_close()
thread.join(5)
def test_auth_refresh_latency_is_excluded_from_query_timing(self):
import lib.request.connect as connect
originalPage, originalDelta = Connect.getPage, connect.calculateDeltaSeconds
def getPage(**kwargs):
getCurrentThreadData().authRefreshTime += 12
return 'ok', None, 200
Connect.getPage = staticmethod(getPage)
connect.calculateDeltaSeconds = lambda start: 15
try:
Connect.queryPage(content=True)
self.assertEqual(getCurrentThreadData().lastQueryDuration, 3)
finally:
Connect.getPage = staticmethod(originalPage)
connect.calculateDeltaSeconds = originalDelta
class TestAuthRefreshLoader(unittest.TestCase):
def setUp(self):
self.directory = tempfile.mkdtemp(prefix='sqlmap-authrefresh-')
self.oldOption = conf.authRefresh
self.oldFunction = kb.authRefreshFunction
self.oldPath = list(sys.path)
def tearDown(self):
conf.authRefresh = self.oldOption
kb.authRefreshFunction = self.oldFunction
sys.path[:] = self.oldPath
for name in list(sys.modules):
if name.startswith('authrefresh_test_'):
del sys.modules[name]
shutil.rmtree(self.directory)
def script(self, name, content):
path = os.path.join(self.directory, 'authrefresh_test_%s.py' % name)
with open(path, 'w') as stream:
stream.write(content)
conf.authRefresh = path
def test_load_does_not_call_refresh(self):
self.script('valid', "def refresh():\n raise AssertionError('must only run on 401')\n")
_setAuthRefreshFunction()
self.assertTrue(callable(kb.authRefreshFunction))
def test_missing_invalid_and_empty_scripts_point_to_template(self):
for name, source in (('empty', ''), ('wrong', 'def other():\n pass\n'), ('args', 'def refresh(req):\n pass\n'), ('syntax', 'def refresh(:')):
self.script(name, source)
with self.assertRaises(SqlmapSyntaxException) as caught:
_setAuthRefreshFunction()
_assertTemplate(self, caught.exception)
conf.authRefresh = os.path.join(self.directory, 'missing.py')
with self.assertRaises(SqlmapFilePathException) as caught:
_setAuthRefreshFunction()
_assertTemplate(self, caught.exception)
def test_help_is_one_line(self):
output = subprocess.check_output([sys.executable, os.path.join(paths.SQLMAP_ROOT_PATH, 'sqlmap.py'), '-hh'])
lines = output.decode('utf-8').splitlines()
matches = [i for i, line in enumerate(lines) if '--auth-refresh' in line]
self.assertEqual(len(matches), 1)
self.assertIn('HTTP 401', lines[matches[0]])
self.assertLessEqual(len(lines[matches[0]]), 78)
for option in ('--preprocess', '--postprocess'):
self.assertLess(matches[0], next(i for i, line in enumerate(lines) if option in line))
if __name__ == '__main__':
unittest.main()
+3
View File
@@ -114,16 +114,19 @@ class TestConfigFileRoundTrip(unittest.TestCase):
conf.url = "http://127.0.0.1:1/?id=1"
conf.commonTables = True
conf.getLinks = True
conf.authRefresh = "refresh_auth.py"
conf.tablePrefix = "zzz"
saveConfig(conf, path)
conf.url, conf.commonTables, conf.tablePrefix = None, False, "overwritten"
conf.getLinks = False
conf.authRefresh = None
configFileParser(path)
self.assertEqual(conf.url, "http://127.0.0.1:1/?id=1")
self.assertTrue(conf.commonTables)
self.assertTrue(conf.getLinks)
self.assertEqual(conf.authRefresh, "refresh_auth.py")
self.assertEqual(conf.tablePrefix, "zzz")
finally:
conf.clear()
+11
View File
@@ -699,6 +699,17 @@ class TestRedirectHandling(unittest.TestCase):
self.assertEqual(calls[0][1], 8443)
self.assertEqual(calls[0][4], "xn--tst-qla.example:8443")
def test_cross_origin_redirect_strips_custom_auth_headers(self):
responses = [(307, [(b"location", b"https://other.test/final")], b""), (200, [], b"ok")]
_, calls = self._run_with_fake_exchange(
responses, url="https://example.test/start",
headers={"X-Auth-Token": "secret", "X-Keep": "yes"},
sensitive_headers=("X-Auth-Token",),
)
names = [key.lower() for key, _ in calls[1][5]]
self.assertNotIn("x-auth-token", names)
self.assertIn("x-keep", names)
class TestPoolRetries(unittest.TestCase):
class _Connection(object):