Repository: StackStorm/st2

Issue:
The api key in the st2api log is not obfuscated
##### SUMMARY
The user found in clean API key in query request (for the load balancer health check)
```GET /api/v1/?st2-api-key=foo HTTP/1.1```

##### ISSUE TYPE
 - Bug Report
 
##### STACKSTORM VERSION
st2 2.10.3, on Python 2.7.12


Discussion:
Can they just set the API key as a header value instead of in the URL? https://docs.stackstorm.com/authentication.html#api-key-usage
@nmaludy Probably, but we should obfuscate it on all our valid methods of providing the key.
Yeah, providing API key as a query param is meant as a last resort for integration with services which don't support sending custom headers, because it's not safe (a lot of external services will log full URL with query params).

Having said that, @bigmstone is correct. We should still not log in in st2api log file.

@jinpingh Thanks for reporting this and good catch.

The log line you provided doesn't look like st2api log line. Is that from load balancer log file? If that's the case, there is nothing we can do about load balancer logging that query param, we can just filter it on the API side.

Here is an example of how st2api log line looks like:

```bash
2019-03-13 09:09:27,601 INFO [-] 9d6e473e-d7da-4e07-bbec-d81af1b4bf95 - GET /v1 with query={'st2-api-key': ['bar']} (method='GET',path='/v1',remote_addr='127.0.0.1',query={'st2-api-key': ['bar']},request_id='9d6e473e-d7da-4e07-bbec-d81af1b4bf95')
2019-03-13 09:09:27,603 INFO [-] 9d6e473e-d7da-4e07-bbec-d81af1b4bf95 - 200 80 1.469ms (method='GET',path='/v1',remote_addr='127.0.0.1',status=200,runtime=1.469,content_length=80,request_id='9d6e473e-d7da-4e07-bbec-d81af1b4bf95')
```

Resolving patch:
diff --git a/st2common/st2common/middleware/logging.py b/st2common/st2common/middleware/logging.py
--- a/st2common/st2common/middleware/logging.py
+++ b/st2common/st2common/middleware/logging.py
@@ -14,16 +14,28 @@
 # limitations under the License.
 
 from __future__ import absolute_import
+
 import time
 import types
 import itertools
 
+from oslo_config import cfg
+
 from st2common.constants.api import REQUEST_ID_HEADER
+from st2common.constants.auth import QUERY_PARAM_ATTRIBUTE_NAME
+from st2common.constants.auth import QUERY_PARAM_API_KEY_ATTRIBUTE_NAME
+from st2common.constants.secrets import MASKED_ATTRIBUTE_VALUE
+from st2common.constants.secrets import MASKED_ATTRIBUTES_BLACKLIST
 from st2common import log as logging
 from st2common.router import Request, NotFoundException
 
 LOG = logging.getLogger(__name__)
 
+SECRET_QUERY_PARAMS = [
+    QUERY_PARAM_ATTRIBUTE_NAME,
+    QUERY_PARAM_API_KEY_ATTRIBUTE_NAME
+] + MASKED_ATTRIBUTES_BLACKLIST
+
 try:
     clock = time.perf_counter
 except AttributeError:
@@ -46,12 +58,20 @@ def __call__(self, environ, start_response):
 
         request = Request(environ)
 
+        query_params = request.GET.dict_of_lists()
+
+        # Mask secret / sensitive query params
+        secret_query_params = SECRET_QUERY_PARAMS + cfg.CONF.log.mask_secrets_blacklist
+        for param_name in secret_query_params:
+            if param_name in query_params:
+                query_params[param_name] = MASKED_ATTRIBUTE_VALUE
+
         # Log the incoming request
         values = {
             'method': request.method,
             'path': request.path,
             'remote_addr': request.remote_addr,
-            'query': request.GET.dict_of_lists(),
+            'query': query_params,
             'request_id': request.headers.get(REQUEST_ID_HEADER, None)
         }
 


Test patch:
diff --git a/st2common/tests/unit/test_logging_middleware.py b/st2common/tests/unit/test_logging_middleware.py
new file mode 100644
--- /dev/null
+++ b/st2common/tests/unit/test_logging_middleware.py
@@ -0,0 +1,74 @@
+# Licensed to the StackStorm, Inc ('StackStorm') under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mock
+import unittest2
+
+from oslo_config import cfg
+
+from st2common.middleware.logging import LoggingMiddleware
+from st2common.constants.secrets import MASKED_ATTRIBUTE_VALUE
+
+__all__ = [
+    'LoggingMiddlewareTestCase'
+]
+
+
+class LoggingMiddlewareTestCase(unittest2.TestCase):
+    @mock.patch('st2common.middleware.logging.LOG')
+    @mock.patch('st2common.middleware.logging.Request')
+    def test_secret_parameters_are_masked_in_log_message(self, mock_request, mock_log):
+
+        def app(environ, custom_start_response):
+            custom_start_response(status='200 OK', headers=[('Content-Length', 100)])
+            return [None]
+
+        router = mock.Mock()
+        endpoint = mock.Mock()
+        router.match.return_value = (endpoint, None)
+        middleware = LoggingMiddleware(app=app, router=router)
+
+        cfg.CONF.set_override(group='log', name='mask_secrets_blacklist',
+                              override=['blacklisted_4', 'blacklisted_5'])
+
+        environ = {}
+        mock_request.return_value.GET.dict_of_lists.return_value = {
+            'foo': 'bar',
+            'bar': 'baz',
+            'x-auth-token': 'secret',
+            'st2-api-key': 'secret',
+            'password': 'secret',
+            'st2_auth_token': 'secret',
+            'token': 'secret',
+            'blacklisted_4': 'super secret',
+            'blacklisted_5': 'super secret',
+        }
+        middleware(environ=environ, start_response=mock.Mock())
+
+        expected_query = {
+            'foo': 'bar',
+            'bar': 'baz',
+            'x-auth-token': MASKED_ATTRIBUTE_VALUE,
+            'st2-api-key': MASKED_ATTRIBUTE_VALUE,
+            'password': MASKED_ATTRIBUTE_VALUE,
+            'token': MASKED_ATTRIBUTE_VALUE,
+            'st2_auth_token': MASKED_ATTRIBUTE_VALUE,
+            'blacklisted_4': MASKED_ATTRIBUTE_VALUE,
+            'blacklisted_5': MASKED_ATTRIBUTE_VALUE,
+        }
+
+        call_kwargs = mock_log.info.call_args_list[0][1]
+        query = call_kwargs['extra']['query']
+        self.assertEqual(query, expected_query)