diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2019-02-04 15:50:29 -0500 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2019-02-06 22:53:16 -0500 |
commit | 30307c4616ad67c01ddae2e1e8e34fabf6028414 (patch) | |
tree | 6e8edd4cb13132aa19f916409f3a3f3dcba7fd0c /lib/sqlalchemy/sql/compiler.py | |
parent | 11845453d76e1576f637161e660160f0a6117af6 (diff) | |
download | sqlalchemy-30307c4616ad67c01ddae2e1e8e34fabf6028414.tar.gz |
Remove all remaining text() coercions and ensure identifiers are safe
Fully removed the behavior of strings passed directly as components of a
:func:`.select` or :class:`.Query` object being coerced to :func:`.text`
constructs automatically; the warning that has been emitted is now an
ArgumentError or in the case of order_by() / group_by() a CompileError.
This has emitted a warning since version 1.0 however its presence continues
to create concerns for the potential of mis-use of this behavior.
Note that public CVEs have been posted for order_by() / group_by() which
are resolved by this commit: CVE-2019-7164 CVE-2019-7548
Added "SQL phrase validation" to key DDL phrases that are accepted as plain
strings, including :paramref:`.ForeignKeyConstraint.on_delete`,
:paramref:`.ForeignKeyConstraint.on_update`,
:paramref:`.ExcludeConstraint.using`,
:paramref:`.ForeignKeyConstraint.initially`, for areas where a series of SQL
keywords only are expected.Any non-space characters that suggest the phrase
would need to be quoted will raise a :class:`.CompileError`. This change
is related to the series of changes committed as part of :ticket:`4481`.
Fixed issue where using an uppercase name for an index type (e.g. GIST,
BTREE, etc. ) or an EXCLUDE constraint would treat it as an identifier to
be quoted, rather than rendering it as is. The new behavior converts these
types to lowercase and ensures they contain only valid SQL characters.
Quoting is applied to :class:`.Function` names, those which are usually but
not necessarily generated from the :attr:`.sql.func` construct, at compile
time if they contain illegal characters, such as spaces or punctuation. The
names are as before treated as case insensitive however, meaning if the
names contain uppercase or mixed case characters, that alone does not
trigger quoting. The case insensitivity is currently maintained for
backwards compatibility.
Fixes: #4481
Fixes: #4473
Fixes: #4467
Change-Id: Ib22a27d62930e24702e2f0f7c74a0473385a08eb
Diffstat (limited to 'lib/sqlalchemy/sql/compiler.py')
-rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 74 |
1 files changed, 62 insertions, 12 deletions
diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index b703c59f2..15ddd7d6f 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -139,8 +139,16 @@ RESERVED_WORDS = set( ) LEGAL_CHARACTERS = re.compile(r"^[A-Z0-9_$]+$", re.I) +LEGAL_CHARACTERS_PLUS_SPACE = re.compile(r"^[A-Z0-9_ $]+$", re.I) ILLEGAL_INITIAL_CHARACTERS = {str(x) for x in range(0, 10)}.union(["$"]) +FK_ON_DELETE = re.compile( + r"^(?:RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT)$", re.I +) +FK_ON_UPDATE = re.compile( + r"^(?:RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT)$", re.I +) +FK_INITIALLY = re.compile(r"^(?:DEFERRED|IMMEDIATE)$", re.I) BIND_PARAMS = re.compile(r"(?<![:\w\$\x5c]):([\w\$]+)(?![:\w\$])", re.UNICODE) BIND_PARAMS_ESC = re.compile(r"\x5c(:[\w\$]*)(?![:\w\$])", re.UNICODE) @@ -758,12 +766,11 @@ class SQLCompiler(Compiled): else: col = with_cols[element.element] except KeyError: - # treat it like text() - util.warn_limited( - "Can't resolve label reference %r; converting to text()", - util.ellipses_string(element.element), + elements._no_text_coercion( + element.element, + exc.CompileError, + "Can't resolve label reference for ORDER BY / GROUP BY.", ) - return self.process(element._text_clause) else: kwargs["render_label_as_label"] = col return self.process( @@ -1076,10 +1083,24 @@ class SQLCompiler(Compiled): if func._has_args: name += "%(expr)s" else: - name = func.name + "%(expr)s" - return ".".join(list(func.packagenames) + [name]) % { - "expr": self.function_argspec(func, **kwargs) - } + name = func.name + name = ( + self.preparer.quote(name) + if self.preparer._requires_quotes_illegal_chars(name) + else name + ) + name = name + "%(expr)s" + return ".".join( + [ + ( + self.preparer.quote(tok) + if self.preparer._requires_quotes_illegal_chars(tok) + else tok + ) + for tok in func.packagenames + ] + + [name] + ) % {"expr": self.function_argspec(func, **kwargs)} def visit_next_value_func(self, next_value, **kw): return self.visit_sequence(next_value.sequence) @@ -3153,9 +3174,13 @@ class DDLCompiler(Compiled): def define_constraint_cascades(self, constraint): text = "" if constraint.ondelete is not None: - text += " ON DELETE %s" % constraint.ondelete + text += " ON DELETE %s" % self.preparer.validate_sql_phrase( + constraint.ondelete, FK_ON_DELETE + ) if constraint.onupdate is not None: - text += " ON UPDATE %s" % constraint.onupdate + text += " ON UPDATE %s" % self.preparer.validate_sql_phrase( + constraint.onupdate, FK_ON_UPDATE + ) return text def define_constraint_deferrability(self, constraint): @@ -3166,7 +3191,9 @@ class DDLCompiler(Compiled): else: text += " NOT DEFERRABLE" if constraint.initially is not None: - text += " INITIALLY %s" % constraint.initially + text += " INITIALLY %s" % self.preparer.validate_sql_phrase( + constraint.initially, FK_INITIALLY + ) return text def define_constraint_match(self, constraint): @@ -3416,6 +3443,24 @@ class IdentifierPreparer(object): return value.replace(self.escape_to_quote, self.escape_quote) + def validate_sql_phrase(self, element, reg): + """keyword sequence filter. + + a filter for elements that are intended to represent keyword sequences, + such as "INITIALLY", "INTIALLY DEFERRED", etc. no special characters + should be present. + + .. versionadded:: 1.3 + + """ + + if element is not None and not reg.match(element): + raise exc.CompileError( + "Unexpected SQL phrase: %r (matching against %r)" + % (element, reg.pattern) + ) + return element + def quote_identifier(self, value): """Quote an identifier. @@ -3439,6 +3484,11 @@ class IdentifierPreparer(object): or (lc_value != value) ) + def _requires_quotes_illegal_chars(self, value): + """Return True if the given identifier requires quoting, but + not taking case convention into account.""" + return not self.legal_characters.match(util.text_type(value)) + def quote_schema(self, schema, force=None): """Conditionally quote a schema name. |