Repository: getredash/redash

Issue:
The API key for one query may be used to retrieve another query's results
### Issue Summary

A query's API key may be used to obtain another query's results via the REST api when including the API key in the query string.

### Steps to Reproduce

1. Create one query and execute it to obtain results (call it query A)
2. Create another query and execute it to obtain different results (call it query B)
3. Get the query's API key for query A (A_API_KEY) and the query number for query A (A_QUERY_NUMBER)
4. Get the result number from query B's most recent run (B_RESULT_NUMBER)
5. Execute the below code and you'll see that the API key for query A can get results from query B

```bash
REDASH_DOMAIN='yourdomain.com'
A_QUERY_NUMBER='query number for query A'
A_API_KEY_A='api key for query A'
B_RESULT_NUMBER='query result number for query b'

# this will download query B's results using query A's access key
wget \
  -O query_b_results.csv \
  "https://$REDASH_DOMAIN/api/queries/$A_QUERY_NUMBER/results/$B_RESULT_NUMBER.csv?api_key=$A_API_KEY"
```

This is a bug because one query's API key should NOT be able to access another query's results.

### Technical details:

* Redash Version: 1.0.3
* Browser/OS: (Command Line) / Linux Mint 18.2
* How did you install Redash: Command line



Discussion:

Resolving patch:
diff --git a/redash/handlers/query_results.py b/redash/handlers/query_results.py
--- a/redash/handlers/query_results.py
+++ b/redash/handlers/query_results.py
@@ -185,14 +185,19 @@ def get(self, query_id=None, query_result_id=None, filetype='json'):
 
         if query_result_id:
             query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query_result_id, self.current_org)
-        elif query_id is not None:
+
+        if query_id is not None:
             query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)
 
-            if query is not None:
+            if query_result is None and query is not None:
                 if settings.ALLOW_PARAMETERS_IN_EMBEDS and parameter_values:
                     query_result = run_query_sync(query.data_source, parameter_values, query.to_dict()['query'], max_age=max_age)
                 elif query.latest_query_data_id is not None:
                     query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query.latest_query_data_id, self.current_org)
+                
+            if query is not None and query_result is not None and self.current_user.is_api_user():
+                if query.query_hash != query_result.query_hash:
+                    abort(404, message='No cached result found for this query.')
 
         if query_result:
             require_access(query_result.data_source.groups, self.current_user, view_only)
@@ -229,7 +234,7 @@ def get(self, query_id=None, query_result_id=None, filetype='json'):
                 self.add_cors_headers(response.headers)
 
             if should_cache:
-                response.headers.add_header('Cache-Control', 'max-age=%d' % ONE_YEAR)
+                response.headers.add_header('Cache-Control', 'private,max-age=%d' % ONE_YEAR)
 
             return response
 


Test patch:
diff --git a/tests/handlers/test_query_results.py b/tests/handlers/test_query_results.py
--- a/tests/handlers/test_query_results.py
+++ b/tests/handlers/test_query_results.py
@@ -118,6 +118,39 @@ def test_has_full_access_to_data_source(self):
         rv = self.make_request('get', '/api/query_results/{}'.format(query_result.id))
         self.assertEquals(rv.status_code, 200)
 
+    def test_access_with_query_api_key(self):
+        ds = self.factory.create_data_source(group=self.factory.org.default_group, view_only=False)
+        query = self.factory.create_query()
+        query_result = self.factory.create_query_result(data_source=ds, query_text=query.query_text)
+
+        rv = self.make_request('get', '/api/queries/{}/results/{}.json?api_key={}'.format(query.id, query_result.id, query.api_key), user=False)
+        self.assertEquals(rv.status_code, 200)
+
+    def test_access_with_query_api_key_without_query_result_id(self):
+        ds = self.factory.create_data_source(group=self.factory.org.default_group, view_only=False)
+        query = self.factory.create_query()
+        query_result = self.factory.create_query_result(data_source=ds, query_text=query.query_text, query_hash=query.query_hash)
+        query.latest_query_data = query_result
+
+        rv = self.make_request('get', '/api/queries/{}/results.json?api_key={}'.format(query.id, query.api_key), user=False)
+        self.assertEquals(rv.status_code, 200)
+    
+    def test_query_api_key_and_different_query_result(self):
+        ds = self.factory.create_data_source(group=self.factory.org.default_group, view_only=False)
+        query = self.factory.create_query(query_text="SELECT 8")
+        query_result2 = self.factory.create_query_result(data_source=ds, query_hash='something-different')
+
+        rv = self.make_request('get', '/api/queries/{}/results/{}.json?api_key={}'.format(query.id, query_result2.id, query.api_key), user=False)
+        self.assertEquals(rv.status_code, 404)
+
+    def test_signed_in_user_and_different_query_result(self):
+        ds2 = self.factory.create_data_source(group=self.factory.org.admin_group, view_only=False)
+        query = self.factory.create_query(query_text="SELECT 8")
+        query_result2 = self.factory.create_query_result(data_source=ds2, query_hash='something-different')
+
+        rv = self.make_request('get', '/api/queries/{}/results/{}.json'.format(query.id, query_result2.id))
+        self.assertEquals(rv.status_code, 403)
+
 
 class TestQueryResultExcelResponse(BaseTestCase):
     def test_renders_excel_file(self):