-
Notifications
You must be signed in to change notification settings - Fork 14.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(templating): Safer Jinja template processing #11704
Changes from 6 commits
7e45c67
e6ff845
cb2fd94
cce6d2b
c11ac0a
e37b20e
aa2bb5d
44a084e
69ddd6d
e1ff657
c0e65ce
bc2fc01
a9461e4
f7fbd59
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ | |
# specific language governing permissions and limitations | ||
# under the License. | ||
"""Defines the templating context for SQL Lab""" | ||
import json | ||
import re | ||
from functools import partial | ||
from typing import Any, Callable, cast, Dict, List, Optional, Tuple, TYPE_CHECKING | ||
|
@@ -48,7 +49,9 @@ | |
"list", | ||
"dict", | ||
"tuple", | ||
"set", | ||
) | ||
COLLECTION_TYPES = ("list", "dict", "tuple", "set") | ||
|
||
|
||
@memoized | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the benefit of memoization here? Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The value doesn't change over time, so just trying to save some processing cycles. Probably not much gain here, but attempting not to repeat initialization logic. |
||
|
@@ -221,10 +224,41 @@ def safe_proxy(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any: | |
value_type=value_type, | ||
) | ||
) | ||
if value_type in COLLECTION_TYPES: | ||
try: | ||
return_value = json.loads(json.dumps(return_value)) | ||
except TypeError: | ||
raise SupersetTemplateException( | ||
_("Unsupported return value for method %(name)s", name=func.__name__,) | ||
) | ||
|
||
return return_value | ||
|
||
|
||
def validate_template_context(context: Dict[str, Any]) -> Dict[str, Any]: | ||
for key in context: | ||
arg_type = type(context[key]).__name__ | ||
if arg_type not in ALLOWED_TYPES and key not in context_addons(): | ||
if arg_type == "partial" and context[key].func.__name__ == "safe_proxy": | ||
continue | ||
raise SupersetTemplateException( | ||
_( | ||
"Unsafe template value for key %(key)s: %(value_type)s", | ||
key=key, | ||
value_type=arg_type, | ||
) | ||
) | ||
if arg_type in COLLECTION_TYPES: | ||
try: | ||
context[key] = json.loads(json.dumps(context[key])) | ||
except TypeError: | ||
raise SupersetTemplateException( | ||
_("Unsupported template value for key %(key)s", key=key,) | ||
) | ||
|
||
return context | ||
|
||
|
||
class BaseTemplateProcessor: # pylint: disable=too-few-public-methods | ||
""" | ||
Base class for database-specific jinja context | ||
|
@@ -256,21 +290,6 @@ def set_context(self, **kwargs: Any) -> None: | |
self._context.update(kwargs) | ||
self._context.update(context_addons()) | ||
|
||
def validate_template_context(self, context: Dict[str, Any]) -> None: | ||
for key in context: | ||
arg_type = type(context[key]).__name__ | ||
if arg_type not in ALLOWED_TYPES and key not in context_addons(): | ||
if arg_type == "partial" and context[key].func.__name__ == "safe_proxy": | ||
continue | ||
|
||
raise SupersetTemplateException( | ||
_( | ||
"Unsafe template value for key %(key)s: %(value_type)s", | ||
key=key, | ||
value_type=arg_type, | ||
) | ||
) | ||
|
||
def process_template(self, sql: str, **kwargs: Any) -> str: | ||
"""Processes a sql template | ||
|
||
|
@@ -281,8 +300,8 @@ def process_template(self, sql: str, **kwargs: Any) -> str: | |
template = self._env.from_string(sql) | ||
kwargs.update(self._context) | ||
|
||
self.validate_template_context(kwargs) | ||
return template.render(kwargs) | ||
context = validate_template_context(kwargs) | ||
return template.render(context) | ||
|
||
|
||
class JinjaTemplateProcessor(BaseTemplateProcessor): | ||
|
@@ -386,13 +405,13 @@ class HiveTemplateProcessor(PrestoTemplateProcessor): | |
|
||
@memoized | ||
def template_processors() -> Dict[str, Any]: | ||
template_processors = current_app.config.get("CUSTOM_TEMPLATE_PROCESSORS", {}) | ||
processors = current_app.config.get("CUSTOM_TEMPLATE_PROCESSORS", {}) | ||
for engine in DEFAULT_PROCESSORS: | ||
# do not overwrite engine-specific CUSTOM_TEMPLATE_PROCESSORS | ||
if not engine in template_processors: | ||
template_processors[engine] = DEFAULT_PROCESSORS[engine] | ||
if not engine in processors: | ||
processors[engine] = DEFAULT_PROCESSORS[engine] | ||
|
||
return template_processors | ||
return processors | ||
|
||
|
||
def get_template_processor( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should prevent
DEFAULT_FEATURE_FLAGS
from being overridden by custom config files/modules.