Repository: incubator-airflow
Updated Branches:
  refs/heads/master b55f41f2c -> daa281c03


[AIRFLOW-1007] Use Jinja sandbox for chart_data endpoint

Right now, users can put in arbitrary strings into
the chart_data
endpoint, and execute arbitrary code using the
chart_data endpoint. By
using literal_eval and
ImmutableSandboxedEnvironment, we can reduce RCE.

Right now, users can put in arbitrary strings into
the chart_data
endpoint, and execute arbitrary code using the
chart_data endpoint. By
using literal_eval and
ImmutableSandboxedEnvironment, we can prevent
RCE.

Dear Airflow maintainers,

Please accept this PR. I understand that it will
not be reviewed until I have checked off all the
steps below!

### JIRA
- [x] My PR addresses the following [Airflow JIRA]
(https://issues.apache.org/jira/browse/AIRFLOW/)
issues and references them in the PR title. For
example, "[AIRFLOW-XXX] My Airflow PR"
    -
https://issues.apache.org/jira/browse/AIRFLOW-1007

### Description
- [x] I changed Jinja to use the
ImmutableSandboxedEnvironment, and used
literal_eval, to limit the amount of RCE.

### Tests
- [x] My PR adds the following unit tests:
SecurityTest chart_data tests

### Commits
- [x] My commits all reference JIRA issues in
their subject lines, and I have squashed multiple
commits if they address the same issue. In
addition, my commits follow the guidelines from
"[How to write a good git commit
message](http://chris.beams.io/posts/git-
commit/)":
    1. Subject is separated from body by a blank line
    2. Subject is limited to 50 characters
    3. Subject does not end with a period
    4. Subject uses the imperative mood ("add", not
"adding")
    5. Body wraps at 72 characters
    6. Body explains "what" and "why", not "how"

to: aoen plypaul artwr  bolkedebruin

Closes #2184 from saguziel/aguziel-jinja-2


Project: http://git-wip-us.apache.org/repos/asf/incubator-airflow/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-airflow/commit/daa281c0
Tree: http://git-wip-us.apache.org/repos/asf/incubator-airflow/tree/daa281c0
Diff: http://git-wip-us.apache.org/repos/asf/incubator-airflow/diff/daa281c0

Branch: refs/heads/master
Commit: daa281c0364609d6812921123cf47e4118b40484
Parents: b55f41f
Author: Alex Guziel <alex.guz...@airbnb.com>
Authored: Mon Apr 3 12:16:00 2017 -0700
Committer: Arthur Wiedmer <arthur.wied...@gmail.com>
Committed: Mon Apr 3 12:16:00 2017 -0700

----------------------------------------------------------------------
 airflow/www/views.py |  7 ++++---
 tests/core.py        | 38 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 42 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/daa281c0/airflow/www/views.py
----------------------------------------------------------------------
diff --git a/airflow/www/views.py b/airflow/www/views.py
index 0def0a9..a9bab31 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -43,7 +43,7 @@ from flask_admin.tools import iterdecode
 from flask_login import flash
 from flask._compat import PY2
 
-import jinja2
+from jinja2.sandbox import ImmutableSandboxedEnvironment
 import markdown
 import nvd3
 
@@ -328,8 +328,9 @@ class Airflow(BaseView):
         request_dict = {k: request.args.get(k) for k in request.args}
         args.update(request_dict)
         args['macros'] = macros
-        sql = jinja2.Template(chart.sql).render(**args)
-        label = jinja2.Template(chart.label).render(**args)
+        sandbox = ImmutableSandboxedEnvironment()
+        sql = sandbox.from_string(chart.sql).render(**args)
+        label = sandbox.from_string(chart.label).render(**args)
         payload['sql_html'] = Markup(highlight(
             sql,
             lexers.SqlLexer(),  # Lexer call

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/daa281c0/tests/core.py
----------------------------------------------------------------------
diff --git a/tests/core.py b/tests/core.py
index bd52d19..997bb42 100644
--- a/tests/core.py
+++ b/tests/core.py
@@ -63,6 +63,8 @@ from airflow.utils.logging import LoggingMixin
 from lxml import html
 from airflow.exceptions import AirflowException
 from airflow.configuration import AirflowConfigException, run_command
+from jinja2.sandbox import SecurityError
+from jinja2 import UndefinedError
 
 import six
 
@@ -1469,6 +1471,42 @@ class SecurityTests(unittest.TestCase):
         response = self.app.get("/admin/log", follow_redirects=True)
         self.assertIn(bleach.clean("<script>alert(123456)</script>"), 
response.data.decode('UTF-8'))
 
+    def test_chart_data_template(self):
+        """Protect chart_data from being able to do RCE."""
+        session = settings.Session()
+        Chart = models.Chart
+        chart1 = Chart(
+            label='insecure_chart',
+            conn_id='airflow_db',
+            chart_type='bar',
+            sql="SELECT {{ ''.__class__.__mro__[1].__subclasses__() }}"
+        )
+        chart2 = Chart(
+            label="{{ ''.__class__.__mro__[1].__subclasses__() }}",
+            conn_id='airflow_db',
+            chart_type='bar',
+            sql="SELECT 1"
+        )
+        chart3 = Chart(
+            label="{{ subprocess.check_output('ls') }}",
+            conn_id='airflow_db',
+            chart_type='bar',
+            sql="SELECT 1"
+        )
+        session.add(chart1)
+        session.add(chart2)
+        session.add(chart3)
+        session.commit()
+        chart1_id = 
session.query(Chart).filter(Chart.label=='insecure_chart').first().id
+        with self.assertRaises(SecurityError):
+            response = 
self.app.get("/admin/airflow/chart_data?chart_id={}".format(chart1_id))
+        chart2_id = session.query(Chart).filter(Chart.label=="{{ 
''.__class__.__mro__[1].__subclasses__() }}").first().id
+        with self.assertRaises(SecurityError):
+            response = 
self.app.get("/admin/airflow/chart_data?chart_id={}".format(chart2_id))
+        chart3_id = session.query(Chart).filter(Chart.label=="{{ 
subprocess.check_output('ls') }}").first().id
+        with self.assertRaises(UndefinedError):
+            response = 
self.app.get("/admin/airflow/chart_data?chart_id={}".format(chart3_id))
+
     def tearDown(self):
         configuration.conf.set("webserver", "expose_config", "False")
         self.dag_bash.clear(start_date=DEFAULT_DATE, end_date=datetime.now())

Reply via email to