Better heuristics for non-SQLi techniques

This commit is contained in:
Miroslav Štampar 2026-08-08 02:14:34 +02:00
parent a8b1bb371d
commit bbe164a7f3
3 changed files with 289 additions and 26 deletions

View file

@ -85,6 +85,9 @@
* Improved the detection of the SQL dialect of the target.
* The switch `--xpath` no longer reports an injection when the page changes only because it shows the payload again. Reflection is not proof that an expression was evaluated.
* sqlmap now makes sure that the random markers around the retrieved data are all different. Two equal markers made the error-based technique give back the wrong character.
* Made the heuristic hints of the non-SQL switches exclusive. A signature no longer matches the errors of a different engine, an ordinary SQL error, or a page that only contains the name of a template engine.
* Corrected the GraphQL validation signatures. They now match the quotes in the way that the JSON body escapes them.
* Added the error signatures of Mako and of DynamoDB. sqlmap did not recognise the errors of these two back-ends.
## Quality

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.8.17"
VERSION = "1.10.8.18"
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)
@ -1008,13 +1008,19 @@ NOSQL_ERRORS = (
("CouchDB", r'"error"\s*:\s*"(?:bad_request|query_parse_error|missing_named_query)"|invalid operator: ?\$'),
("Elasticsearch", r'"type"\s*:\s*"[a-z_]*?(?:query_shard|x_content_parse|parsing|search_phase_execution|illegal_argument|too_many_clauses|number_format|script)_exception"|Failed to parse query \['),
("Solr", r"org\.apache\.solr\.[\w.]*(?:SyntaxError|SolrException)"),
("Neo4j", r"Neo\.(?:ClientError|DatabaseError|TransientError|ClientNotification)\.|\bNeo4jError\b|even number of non-escaped quotes|Failed to parse string literal|expected an expression|'(?:UNWIND|OPTIONAL|DETACH|FOREACH|MERGE|LOAD CSV)'"),
# NOTE: 'MERGE' is not Cypher-only. It is standard SQL, so it matched "Incorrect syntax near 'MERGE'"
("Neo4j", r"Neo\.(?:ClientError|DatabaseError|TransientError|ClientNotification)\.|\bNeo4jError\b|even number of non-escaped quotes|Failed to parse string literal|expected an expression|'(?:UNWIND|OPTIONAL|DETACH|FOREACH|LOAD CSV)'"),
("ArangoDB", r"\bArangoError\b|AQL: (?:syntax|parse) error"),
("Cassandra", r"line \d+:\d+ (?:no viable alternative at input|(?:mismatched|extraneous) input '.*?' expecting)|org\.apache\.cassandra|com\.datastax|\bInvalid(?:Request|Query)Exception\b"),
# NOTE: the ANTLR "line N:M no viable alternative" line is not evidence of Cassandra on its own -
# Hibernate 6 and Trino emit it word for word. What IS particular to the CQL grammar: the driver
# exception in front of it, the ANTLR3 lexer wording ('mismatched character', which the ANTLR4
# parsers do not have), and the "(...[TOKEN]...)" excerpt that CQL appends to the offending token
("Cassandra", r"\b(?:ResponseError|SyntaxException|InvalidRequestException|InvalidQueryException)\b[^\n]{0,60}?line \d+:\d+ (?:no viable alternative at input|(?:mismatched|extraneous) input)|line \d+:\d+ (?:mismatched character|no viable alternative at input '[^']*' \(\.\.\.)|org\.apache\.cassandra|com\.datastax|\bInvalid(?:Request|Query)Exception\b"),
("Redis", r"\bWRONGTYPE\b|ERR Error (?:compiling|running) script|@user_script|\bReplyError\b"),
("Memcached", r"CLIENT_ERROR bad|SERVER_ERROR object too large"),
("InfluxDB", r"error parsing query|unable to parse '[^']*': found"),
("HBase/Phoenix", r"org\.apache\.phoenix|PhoenixParserException|org\.apache\.hadoop\.hbase"),
("DynamoDB", r"Statement wasn't well formed, can't be processed|software\.amazon\.awssdk\.services\.dynamodb|com\.amazonaws\.services\.dynamodbv2|\bDynamoDb(?:Exception|Error)\b"),
)
NOSQL_ERROR_REGEX = "(?:%s)" % '|'.join(regex for _, regex in NOSQL_ERRORS)
@ -1087,14 +1093,17 @@ GRAPHQL_PARSE_ERRORS = (
r"\bExpected Name,\s*found\b",
r"\bUnexpected\s+<EOF>\b",
)
# NOTE: graphql-js quotes the offending name, and the response carries those quotes backslash-escaped
# inside the JSON body ('Cannot query field \"x\" on type \"Query\"'). Without the optional backslash
# none of these ever matched a real answer - only a pretty-printed one
GRAPHQL_VALIDATION_ERRORS = (
r'"code"\s*:\s*"GRAPHQL_VALIDATION_FAILED"',
r"\bCannot query field\s+\"[^\"]+\"\s+on type\s+\"[^\"]+\"",
r"\bUnknown argument\s+\"[^\"]+\"\s+on field\s+\"[^\"]+\"",
r"\bField\s+\"[^\"]+\"\s+argument\s+\"[^\"]+\"\s+of type\s+\"[^\"]+\"\s+is required\b",
r"\bVariable\s+\"\$[^\"]+\"\s+got invalid value\b",
r"\bCannot query field\s+\\?\"[^\"\\]+\\?\"\s+on type\s+\\?\"[^\"\\]+\\?\"",
r"\bUnknown argument\s+\\?\"[^\"\\]+\\?\"\s+on field\s+\\?\"[^\"\\]+\\?\"",
r"\bField\s+\\?\"[^\"\\]+\\?\"\s+argument\s+\\?\"[^\"\\]+\\?\"\s+of type\s+\\?\"[^\"\\]+\\?\"\s+is required\b",
r"\bVariable\s+\\?\"\$[^\"\\]+\\?\"\s+got invalid value\b",
r"\bExpected type\s+[^,]+,\s*found\b",
r"\bDid you mean\s+\"[^\"]+\"\b",
r"\bDid you mean\s+\\?\"[^\"\\]+\\?\"",
)
GRAPHQL_APQ_ERRORS = (
r"\bPersistedQueryNotFound\b",
@ -1150,17 +1159,25 @@ LDAP_FINGERPRINT_ATTRIBUTES = (
# fingerprinting (matched against HTTP response bodies). Each tuple is
# (backend_name, regex_fragment).
XPATH_ERROR_SIGNATURES = (
("Java JAXP / Xalan", r"(?:javax\.xml\.(?:xpath\.XPathExpressionException|transform\.Transformer(?:Configuration)?Exception)|com\.sun\.org\.apache\.xpath\.(?:XPathException|XPathProcessorException)|org\.apache\.xpath|org\.xml\.sax\.SAX(?:Parse)?Exception)"),
# NOTE: neither javax.xml.transform.Transformer*Exception nor org.xml.sax.SAX*Exception belongs
# here. The first is the XSLT transformer and the second is the XML parser, so claiming them made
# every stylesheet failure and every malformed-XML response suggest '--xpath' as well. A real
# Xalan XPath failure always carries javax.xml.xpath.XPathExpressionException
("Java JAXP / Xalan", r"(?:javax\.xml\.xpath\.XPathExpressionException|com\.sun\.org\.apache\.xpath\.(?:XPathException|XPathProcessorException)|org\.apache\.xpath)"),
("Java JAXP / Xalan", r"XPath (?:expression|syntax) error"),
("Java JAXP / Saxon", r"net\.sf\.saxon\.(?:trans\.XPathException|s9api\.SaxonApiException)"),
("Java JAXP / Saxon", r"(?:XPST|XPTY|XPDY|XQST|XTDE)\d{4}:"),
(".NET XPathNavigator", r"System\.Xml\.(?:XPath\.XPathException|XmlException)"),
# NOTE: XTDE is an XSLT Transformation Dynamic Error, so it stays with '--xslt'
("Java JAXP / Saxon", r"(?:XPST|XPTY|XPDY|XQST)\d{4}:"),
(".NET XPathNavigator", r"System\.Xml\.XPath\.XPathException"),
(".NET XPathNavigator", r"Expression must evaluate to a node-set"),
(".NET XPathNavigator", r"has an invalid (?:token|qualified name)"),
("lxml / libxml2", r"(?:lxml\.etree\.(?:XPath(?:Eval|Document|Syntax)?Error)|libxml2|xmlXPath(?:CompOp|Eval|Err))"),
("lxml / libxml2", r"(?:XPath error|Invalid (?:expression|predicate))"),
# NOTE: 'Invalid expression' on its own is not an XPath error. libxml2 always prefixes it with
# "XPath error : ", and PHP always names the failing method, so both are covered without the
# bare form - which otherwise matched any calculator or formula field
("lxml / libxml2", r"XPath error"),
("PHP SimpleXML / DOMXPath", r"(?:SimpleXMLElement::xpath\(\)|DOMXPath::(?:query|evaluate)\(\))"),
("PHP SimpleXML / DOMXPath", r"Invalid expression|xmlXPathEval"),
("PHP SimpleXML / DOMXPath", r"xmlXPathEval"),
("Saxon (standalone)", r"(?:net\.sf\.saxon\.(?:s9api\.SaxonApiException|trans\.XPathException)|Saxon error)"),
("Saxon (standalone)", r"Static error\(s\) in query"),
("BaseX", r"org\.basex\.(?:query\.QueryException|core\.BaseXException)"),
@ -1226,14 +1243,22 @@ XQUERY_HARVEST_CHARS = 32
# the binding that actually tells the tester what they are talking to.
XSLT_ERROR_SIGNATURES = (
("PHP XSLTProcessor", r"XSLTProcessor::(?:importStylesheet|transformTo\w+)\(\)"),
("libxslt / lxml", r"lxml\.etree\.(?:XSLT(?:Parse|Apply|)Error|XPathEvalError)"),
("Saxon", r"(?:net\.sf\.saxon\.|SaxonApiException|Static error(?:s)? (?:in|at)|XTDE\d{4}|XTSE\d{4})"),
("Xalan / Java JAXP", r"(?:javax\.xml\.transform\.Transformer(?:Configuration)?Exception|org\.apache\.xalan|XSLT Error)"),
# NOTE: XPathEvalError is raised by a plain tree.xpath() call, so it belongs to '--xpath'. lxml
# raises XSLTParseError / XSLTApplyError for a stylesheet
("libxslt / lxml", r"lxml\.etree\.XSLT(?:Parse|Apply)?Error"),
# NOTE: Saxon is one product for XPath, XQuery and XSLT, so the package name alone proves nothing.
# Only the XSLT-exclusive evidence is kept (XTSE static / XTDE dynamic codes, the style package)
("Saxon", r"(?:net\.sf\.saxon\.style\.|XTDE\d{4}|XTSE\d{4}|Failed to compile stylesheet)"),
# NOTE: only the 'Configuration' form is exclusive to a stylesheet. javax.xml.xpath wraps a plain
# XPath failure in a bare TransformerException, and a Xalan run-time failure names org.apache.xalan
("Xalan / Java JAXP", r"(?:javax\.xml\.transform\.TransformerConfigurationException|org\.apache\.xalan|XSLT Error)"),
(".NET XslCompiledTransform", r"System\.Xml\.Xsl\.(?:XslLoadException|XsltException)"),
# Anchored to XSLT vocabulary on purpose: this regex also drives the GLOBAL heuristic hint in
# checks.py, and bare "compilation error" / "Invalid expression" match gcc, javac and regex failures,
# which would suggest '--xslt' on targets that have nothing to do with XSLT.
("libxslt", r"(?:xsltParseStylesheet|xsltApplyStylesheet|xsltCompilePattern|xsl:\w+ : |xmlXPathEval|XPath error : )"),
# NOTE: xmlXPathEval is the libxml2 XPath entry point, which '--xpath' owns. libxslt names the
# stylesheet file instead, and that is what tells the two apart
("libxslt", r"(?:xsltParseStylesheet|xsltApplyStylesheet|xsltCompilePattern|xsltLoadStylesheet|xsl:\w+ : |(?:runtime|compilation) error: file [^\n]{0,120}\.xsl)"),
("Generic XSLT", r"(?:XSLT|xsl:stylesheet).{0,40}?(?:error|exception|fail)"),
)
@ -1274,15 +1299,25 @@ XSLT_MAX_HARVEST = 12
# Each tuple is (engine_name, regex_fragment).
SSTI_ERROR_SIGNATURES = (
("Jinja2", r"jinja2\.exceptions\.\w+|TemplateSyntaxError|UndefinedError|TemplateNotFound|TemplateAssertionError"),
("Twig", r"Twig[\\_]Error|Twig[\\_]Environment|Unknown (?:filter|function|test|tag)"),
("Freemarker", r"freemarker\.(?:core|template|extract|cache)\.\w+|ParseException|InvalidReferenceException|TemplateException"),
# NOTE: 'at line: N char: N' (with those colons) is how Mako, and only Mako, points at the fault
("Mako", r"mako\.exceptions\.\w+|at line: \d+ char: \d+"),
# NOTE: Twig quotes the offending name ('Unknown "upper" filter'). Without the quotes this also
# matched the 'Unknown function' of Neo4j and Cassandra, which suggested '--ssti' on a NoSQL error
("Twig", "Twig[\\\\_]Error|Twig[\\\\_]Environment|Unknown \"[^\"]+\" (?:filter|function|test|tag)"),
# NOTE: a bare 'ParseException' is not Freemarker. It also matched java.text.ParseException,
# org.xml.sax.SAXParseException and REXML::ParseException. The package prefix is always present
("Freemarker", r"freemarker\.(?:core|template|extract|cache)\.\w+|InvalidReferenceException|TemplateException"),
("Velocity", r"org\.apache\.velocity\.(?:runtime|exception)\.\w+|ParseErrorException|MethodInvocationException|ResourceNotFoundException"),
("Spring EL / Thymeleaf", r"org\.springframework\.expression\.\w+|org\.thymeleaf\.\w+|SpelEvaluationException|TemplateProcessingException|ExpressionParsingException"),
("Struts2 (OGNL)", r"ognl\.(?:OgnlException|NoSuchPropertyException|MethodFailedException|InappropriateExpressionException|ExpressionSyntaxException)|com\.opensymphony\.xwork2|org\.apache\.struts2|There is no Action mapped for|Struts (?:Problem Report|has detected an unhandled exception)"),
("ERB", r"\(erb\):\d+|NameError.*undefined local variable"),
("Pug/Jade", r"pug|jade|ParseError"),
("Handlebars", r"handlebars|Handlebars|Parse error on line"),
("Generic SSTI", r"template.*?(?:error|syntax|exception)"),
# NOTE: these must stay anchored to a diagnostic. The bare product names matched any page that
# carries the word 'pug'/'jade'/'handlebars' (a surname, a colour, a <script src=> of the runtime),
# and the bare 'ParseError' matched lxml.etree.XSLTParseError and ElementTree.ParseError
("Pug/Jade", "\\.(?:pug|jade):\\d+|(?:Pug|Jade):\\d+|unexpected token \"(?:indent|outdent|start-attributes|interpolation|attrs)\""),
("Handlebars", r"handlebars[^\n]{0,60}?(?:error|exception)|Parse error on line \d+"),
# NOTE: 'template.*?error' matched any line holding both words, down to a CSS comment
("Generic SSTI", r"\bTemplate(?:Syntax|Render|Assertion|Parse)?(?:Error|Exception)\b|\btemplate[^\n]{0,40}?(?:syntax error|render error|parse error|error on line)|\b(?:syntax|parse) error in template\b"),
)
SSTI_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in SSTI_ERROR_SIGNATURES)
@ -1294,12 +1329,17 @@ SSTI_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in SSTI_ERROR_SIG
XXE_ERROR_SIGNATURES = (
("libxml2 (PHP/lxml)", r"(?:failed to load (?:external entity|\")|xmlParseEntityRef|Entity '[^']*' not defined|EntityRef: expecting|Detected an entity reference loop|String not started expecting|StartTag: invalid element name|Start tag expected|Extra content at the end of the document|Premature end of data|error parsing DTD|internal error: Huge input lookup)"),
("PHP simplexml/DOM", r"(?:simplexml_load_string\(\)|DOMDocument::load(?:XML)?\(\)|SimpleXMLElement::__construct\(\))"),
("Java (Xerces/JAXP)", r"(?:org\.xml\.sax\.SAXParseException|com\.sun\.org\.apache\.xerces|javax\.xml\.stream\.XMLStreamException|The (?:entity|element type) \"[^\"]*\" was referenced|DOCTYPE is disallowed when the feature|External (?:DTD|parsed entities|Entity): failed|must be declared|had to be read but the maximum)"),
# NOTE: 'must be declared' has to keep the quoted name in front of it. On its own it matched the
# "variable 'x' must be declared before it is used" of TypeScript and of every other compiler
("Java (Xerces/JAXP)", r"(?:org\.xml\.sax\.SAXParseException|com\.sun\.org\.apache\.xerces|javax\.xml\.stream\.XMLStreamException|The (?:entity|element type) \"[^\"]*\" was referenced|DOCTYPE is disallowed when the feature|External (?:DTD|parsed entities|Entity): failed|\"[^\"]*\" must be declared|had to be read but the maximum)"),
(".NET System.Xml", r"(?:System\.Xml\.XmlException|For security reasons DTD is prohibited|Reference to undeclared entity|An error occurred while parsing EntityName|XmlTextReaderImpl)"),
("Python expat", r"(?:xml\.parsers\.expat\.ExpatError|undefined entity|not well-formed \(invalid token\)|ExpatError)"),
("Ruby Nokogiri/REXML", r"(?:Nokogiri::XML::SyntaxError|REXML::ParseException|Entity .* not defined)"),
("Go encoding/xml", r"XML syntax error on line \d+"),
("Generic XML", r"(?:XML (?:parsing|parse|syntax) error|malformed XML|unexpected (?:end of|<) )"),
# NOTE: 'unexpected end of ...' is what every parser says, not what an XML parser says. It matched
# the "Unexpected end of query" of BaseX, the "Unexpected <EOF>" of GraphQL and the "Unexpected end
# of file" of Freemarker. libxml2 says "Premature end of data", which is covered above
("Generic XML", r"(?:XML (?:parsing|parse|syntax) error|malformed XML)"),
)
XXE_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in XXE_ERROR_SIGNATURES)
@ -1343,8 +1383,11 @@ XXE_LOCATION_SWEEP_MAX = 12
# which is what distinguishes HQL injection from ordinary SQL injection.
HQL_ERROR_SIGNATURES = (
("Hibernate", r"org\.hibernate\.(?:query|hql|QueryException|exception\.SQLGrammarException)"),
("Hibernate", r"(?:QuerySyntaxException|QueryException|SemanticException|PathElementException|UnknownEntityException|InterpretationException)"),
("Hibernate", r"(?:token recognition error at|unexpected (?:token|end of subtree|AST node)|Could not (?:resolve|interpret) (?:attribute|root entity|path|property)|line \d+:\d+ (?:no viable alternative|mismatched input|token recognition error))"),
# NOTE: a bare 'QueryException' belongs to org.basex.query too (an XQuery engine), and the bare
# ANTLR "line N:M ..." line is emitted by Cassandra and Trino as well. Both are dropped: the
# package-qualified fragment above already covers org.hibernate.QueryException
("Hibernate", r"(?:QuerySyntaxException|SemanticException|PathElementException|UnknownEntityException|InterpretationException)"),
("Hibernate", r"(?:unexpected (?:token:|end of subtree|AST node)|Could not (?:resolve|interpret) (?:attribute|root entity|path|property))"),
("EclipseLink / JPQL", r"(?:org\.eclipse\.persistence\.exceptions\.JPQLException|Exception \[EclipseLink|Problem compiling \[|An exception occurred while creating a query)"),
("JPA / JPQL", r"(?:javax|jakarta)\.persistence\.(?:PersistenceException|Query(?:Syntax|Timeout)?Exception)"),
("Generic HQL/JPQL", r"(?:HQL|JPQL|EJBQL)\b.*?(?:error|exception|syntax|not (?:mapped|resolve))"),

View file

@ -0,0 +1,217 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Cross-engine invariants of the non-SQL heuristic signatures in lib/core/settings.py.
heuristicCheckSqlInjection() tries every one of these regexes against the same response and prints a
hint for each one that matches. There is no order and no priority, so a signature that is a little too
generous does not merely lose precision - it makes sqlmap recommend a switch that has nothing to do
with the target. The two ways that happens:
overlap one engine claims another engine's error (a stylesheet failure suggesting '--xpath',
a Hibernate failure suggesting '--nosql', an XML parser failure suggesting '--xpath').
false positive a signature matches a page that is not an error at all - a surname that contains
'pug', a <script src=> of the handlebars runtime, a CSS comment holding the words
'template' and 'error', or a plain SQL injection error.
The corpus below is real error output, one entry per back-end, labelled with the engine that owns it.
Every engine must recognise its own errors and nothing else.
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""
import os
import re
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.settings import GRAPHQL_ERROR_REGEX
from lib.core.settings import HQL_ERROR_REGEX
from lib.core.settings import LDAP_ERROR_REGEX
from lib.core.settings import NOSQL_ERROR_REGEX
from lib.core.settings import SSTI_ERROR_REGEX
from lib.core.settings import XPATH_ERROR_REGEX
from lib.core.settings import XSLT_ERROR_REGEX
from lib.core.settings import XXE_ERROR_REGEX
ENGINES = (
("nosql", NOSQL_ERROR_REGEX),
("graphql", GRAPHQL_ERROR_REGEX),
("ldap", LDAP_ERROR_REGEX),
("xpath", XPATH_ERROR_REGEX),
("ssti", SSTI_ERROR_REGEX),
("hql", HQL_ERROR_REGEX),
("xslt", XSLT_ERROR_REGEX),
("xxe", XXE_ERROR_REGEX),
)
# (owning engine, back-end, verbatim error output). NOBODY means no engine may match it
NOBODY = "-"
CORPUS = (
("xpath", "lxml", "lxml.etree.XPathEvalError: Invalid expression"),
("xpath", "Java Xalan", "javax.xml.xpath.XPathExpressionException: javax.xml.transform.TransformerException: Extra illegal tokens: ''', 1, ')'"),
("xpath", "Saxon", "net.sf.saxon.trans.XPathException: Unexpected token \"'\" at start of expression"),
("xpath", ".NET", "System.Xml.XPath.XPathException: 'user/*[' has an invalid token."),
("xpath", "PHP DOMXPath", "Warning: DOMXPath::query(): Invalid expression in /var/www/html/search.php on line 22"),
("xpath", "PHP SimpleXML", "Warning: SimpleXMLElement::xpath(): Invalid expression in /var/www/html/s.php on line 12"),
("xpath", "BaseX", "org.basex.query.QueryException: [XPST0003] Unexpected end of query."),
("xpath", "eXist", "org.exist.xquery.XPathException: exerr:ERROR Invalid expression"),
("xpath", "libxml2", "xmlXPathEval: evaluation failed"),
("xslt", "PHP XSLTProcessor", "Warning: XSLTProcessor::importStylesheet(): compilation error: file /tmp/s.xsl line 3 element value-of"),
("xslt", "libxslt parse", "xsltParseStylesheetProcess : document is not a stylesheet"),
("xslt", "lxml", "lxml.etree.XSLTParseError: xsl:value-of : could not compile select expression 'foo('"),
("xslt", "Saxon", "Static error at char 5 in expression: XTDE0640: Circularity"),
("xslt", ".NET", "System.Xml.Xsl.XslLoadException: 'x(' is an invalid XPath expression."),
("xslt", "Java Transformer", "javax.xml.transform.TransformerConfigurationException: Could not compile stylesheet"),
("xslt", "libxslt runtime", "runtime error: file s.xsl line 4 element value-of\nXPath error : Invalid expression\n"),
("xxe", "PHP libxml2", "Warning: simplexml_load_string(): I/O warning : failed to load external entity \"file:///nonexistent\""),
("xxe", "Java Xerces", "org.xml.sax.SAXParseException; lineNumber: 1; columnNumber: 10; DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true."),
("xxe", "Python expat", "xml.parsers.expat.ExpatError: undefined entity: line 2, column 10"),
("xxe", ".NET", "System.Xml.XmlException: Reference to undeclared entity 'xxe'."),
("xxe", "Nokogiri", "Nokogiri::XML::SyntaxError: Entity 'xxe' not defined"),
("xxe", "Go", "XML syntax error on line 3: unexpected EOF"),
("ssti", "Jinja2", "jinja2.exceptions.TemplateSyntaxError: unexpected '}'"),
("ssti", "Twig", "Twig\\Error\\SyntaxError: Unknown \"foo\" filter in \"index.twig\" at line 3."),
("ssti", "Freemarker", "freemarker.core.ParseException: Encountered \"}\" at line 1, column 14"),
("ssti", "Velocity", "org.apache.velocity.exception.ParseErrorException: Encountered '}'"),
("ssti", "Spring EL", "org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'x' cannot be found"),
("ssti", "Struts2 OGNL", "ognl.OgnlException: target is null for setProperty(null, \"x\", [Ljava.lang.String;)"),
("ssti", "ERB", "(erb):1:in `<main>': undefined local variable or method `x'"),
("ssti", "Handlebars", "Error: Parse error on line 1:\n{{#x}}\n-----^\nExpecting 'ID'"),
("hql", "Hibernate 5", "org.hibernate.hql.internal.ast.QuerySyntaxException: unexpected token: ' near line 1, column 42"),
("hql", "Hibernate 6", "org.hibernate.query.SemanticException: line 1:25 no viable alternative at input 'from User where name='"),
("hql", "EclipseLink", "Exception [EclipseLink-8025] : Problem compiling [SELECT u FROM User u WHERE u.name=']"),
("hql", "JPA", "jakarta.persistence.PersistenceException: org.hibernate.QueryException: unexpected char"),
("nosql", "MongoDB", "MongoServerError: unknown top level operator: $where"),
("nosql", "Mongoose", "CastError: Cast to ObjectId failed for value \"x'\" at path \"_id\""),
("nosql", "Cassandra", "InvalidRequestException: line 1:23 no viable alternative at input '''"),
("nosql", "Cassandra driver", "ResponseError: line 1:38 no viable alternative at input"),
("nosql", "Neo4j", "Neo.ClientError.Statement.SyntaxError: Invalid input ''': expected an expression"),
("nosql", "Neo4j function", "Neo.ClientError.Statement.SyntaxError: Unknown function 'foo'"),
("nosql", "Redis", "WRONGTYPE Operation against a key holding the wrong kind of value"),
("nosql", "Elasticsearch", "{\"error\":{\"root_cause\":[{\"type\":\"query_shard_exception\"}]}}"),
("nosql", "CouchDB", "{\"error\":\"query_parse_error\",\"reason\":\"Bad special field name\"}"),
# the quotes arrive backslash-escaped, because the message is a JSON string value
("graphql", "graphql-js parse", "{\"errors\":[{\"message\":\"Syntax Error: Expected Name, found <EOF>.\"}]}"),
("graphql", "graphql-js validation", "{\"errors\":[{\"message\":\"Cannot query field \\\"foo\\\" on type \\\"Query\\\". Did you mean \\\"food\\\"?\"}]}"),
("graphql", "Apollo APQ", "{\"errors\":[{\"message\":\"PersistedQueryNotFound\"}]}"),
("ldap", "Java JNDI", "javax.naming.directory.InvalidSearchFilterException: Missing 'equal' in filter"),
("ldap", "Active Directory", "LdapErr: DSID-0C0906DC, comment: In order to perform this operation a successful bind must be completed"),
("ldap", "OpenLDAP", "ldap_search_ext: Bad search filter (-7)"),
("ldap", "python-ldap", "ldap.FILTER_ERROR: {'desc': 'Bad search filter'}"),
("ldap", "ApacheDS", "org.apache.directory.api.ldap.model.exception.LdapInvalidSearchFilterException"),
# Recorded off real back-ends, not written from memory. Each one is the verbatim answer of a
# deliberately vulnerable front-end to a payload that breaks the syntax of its engine
("ssti", "jinja2 (live)", "TemplateSyntaxError: unexpected 'end of template'"),
("ssti", "mako (live)", "SyntaxException: Expected: %> at line: 1 char: 7"),
("ssti", "twig (live)", "Twig\\Error\\SyntaxError: Unexpected token \"end of template\" of value \"\" in \"__string_template__9bfdeabc\" at line 1."),
("ssti", "pug (live)", "Error: Pug:1:11 > 1"),
("ssti", "handlebars (live)", "Error: Parse error on line 1: Hello {{ --------^ Expecting 'ID', 'STRING', 'NUMBER', got 'EOF'"),
("ssti", "thymeleaf (live)", "org.springframework.expression.ParseException: Expression [Hello ${] @6: No ending suffix '}' for expression starting at character 6: ${"),
("ssti", "freemarker (live)", "freemarker.core.ParseException: Syntax error in template \"inj\" in line 1, column 8: Unexpected end of file reached."),
("ssti", "velocity (live)", "org.apache.velocity.exception.ParseErrorException: Encountered \"x\" at inj[line 1, column 11] Was expecting one of: \"[\" ... \"{\" ... \"(\""),
("ssti", "struts2 (live)", "ognl.ExpressionSyntaxException: Malformed OGNL expression:"),
("nosql", "neo4j (live)", "error: Failed to parse string literal. The query must contain an even number of non-escaped quotes. (line 1, column 63 (offset: 62))"),
("nosql", "arango (live)", "error: ArangoDB error: \"AQL: syntax error, unexpected ) near ')' RETURN u' at position 1:65 (while parsing)\""),
("nosql", "cassandra (live)", "error: line 1:76 mismatched character ' ' expecting '''"),
("nosql", "cassandra alt (live)", "error: line 1:60 no viable alternative at input 'OR' (...lab.users WHERE username='' [OR]...)"),
("nosql", "elasticsearch (live)", "error: Elasticsearch error: {\"root_cause\":[{\"type\":\"query_shard_exception\",\"reason\":\"Failed to parse query [username:luther]\"}]}"),
("nosql", "redis (live)", "error: ERR Error compiling script (new function): user_script:1: unfinished string near ' '"),
("nosql", "solr (live)", "error: Solr error: {\"metadata\":[\"error-class\",\"org.apache.solr.common.SolrException\"]}"),
("nosql", "dynamodb (live)", "error: Statement wasn't well formed, can't be processed: SELECT * FROM users WHERE username = 'luther' AND password = '''"),
("graphql", "graphql (live)", "errors\":[{\"message\":\"Cannot parse query\",\"extensions\":{\"code\":\"GRAPHQL_PARSE_FAILED\"}}]}"),
("ldap", "openldap (live)", "errorMessage\":\"Bad search filter (-7): {'result': -7, 'desc': 'Bad search filter', 'errno': 11}"),
("xpath", "lxml (live)", "error\":\"XPathEvalError: Invalid expression\"}"),
("xpath", "xquery (live)", "error Stopped at /app/, 3/25: [XPST0003] Expecting function argument, found ':'."),
("xslt", "libxslt (live)", "XSLT error StartTag: invalid element name, line 4, column 45 ( , line 4)"),
("xxe", "libxml2 (live)", "Parsed document; content: Parser warnings: failed to load \"file:///nonexistent\": No such file or directory"),
# a plain SQL injection error belongs to the SQL engine. No non-SQL switch may claim it
(NOBODY, "MySQL", "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version"),
(NOBODY, "Microsoft SQL Server", "Incorrect syntax near 'MERGE'."),
(NOBODY, "Oracle", "ORA-00933: SQL command not properly ended"),
(NOBODY, "PostgreSQL", "ERROR: unterminated quoted string at or near \"'\""),
(NOBODY, "SQLite", "sqlite3.OperationalError: unrecognized token: \"'\""),
(NOBODY, "Trino", "io.trino.sql.parser.ParsingException: line 1:15: mismatched input 'FROM'. Expecting: '(', <query>"),
# ordinary pages and unrelated tool output
(NOBODY, "gcc", "main.c:5:9: error: expected ';' before '}' token"),
(NOBODY, "python re", "re.error: bad character range a-Z at position 4"),
(NOBODY, "java date", "java.text.ParseException: Unparseable date: \"2026-13-45\""),
(NOBODY, "TypeScript", "error TS2448: Block-scoped variable 'x' must be declared before it is used."),
(NOBODY, "surname", "<td>Contact: Mrs. Puget</td><td>Sales</td>"),
(NOBODY, "asset (pug)", "<script src=\"/static/js/pug-runtime.min.js\"></script>"),
(NOBODY, "asset (handlebars)", "<script src=\"/assets/handlebars.runtime-v4.7.7.js\"></script>"),
(NOBODY, "colour name", "<option value=\"jade\">Jade green</option>"),
(NOBODY, "error page", "<h1>500 Internal Server Error</h1><p>The server encountered an internal error.</p>"),
(NOBODY, "CSS comment", "/* template error state: .alert{color:red} */"),
(NOBODY, "documentation", "This page explains how to use a query exception handler in your application."),
(NOBODY, "calculator", "Invalid expression: 2++2"),
)
# The only overlaps that are allowed, because the error text really does belong to both. libxslt and
# .NET report the failure of an XPath expression that sits inside a stylesheet, so both switches are
# worth trying - which is what the response says. Anything else is a bug in the signatures.
ACCEPTED_SHARED = {
("xslt", ".NET"): ("xpath",),
("xslt", "libxslt runtime"): ("xpath",),
# breaking a stylesheet also breaks the XML that carries it, so libxml2 reports a malformed
# document as well. Harmless: the XXE hint needs an XML/SOAP request body to fire at all
("xslt", "libxslt (live)"): ("xxe",),
}
def _matches(text):
return tuple(name for name, regex in ENGINES if re.search(regex, text))
class HeuristicSignatureTest(unittest.TestCase):
def test_each_engine_recognises_its_own_errors(self):
for owner, backend, text in CORPUS:
if owner == NOBODY:
continue
self.assertIn(owner, _matches(text),
msg="'--%s' no longer recognises its own %s error: %r" % (owner, backend, text))
def test_no_engine_claims_another_engines_error(self):
for owner, backend, text in CORPUS:
if owner == NOBODY:
continue
allowed = ACCEPTED_SHARED.get((owner, backend), ())
stolen = [_ for _ in _matches(text) if _ != owner and _ not in allowed]
self.assertEqual(stolen, [],
msg="a %s %s error also suggests %s" % (owner, backend, '/'.join("'--%s'" % _ for _ in stolen)))
def test_nothing_fires_on_sql_errors_or_ordinary_pages(self):
for owner, backend, text in CORPUS:
if owner != NOBODY:
continue
fired = _matches(text)
self.assertEqual(fired, (),
msg="%s output suggests %s: %r" % (backend, '/'.join("'--%s'" % _ for _ in fired), text))
def test_every_engine_is_covered(self):
# a new switch must arrive here with its own errors, or the matrix above proves nothing about it
owners = set(owner for owner, _, _ in CORPUS)
missing = set(name for name, _ in ENGINES) - owners
self.assertEqual(missing, set(), msg="engines with no corpus entry: %s" % missing)
if __name__ == "__main__":
unittest.main(verbosity=2)