Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions googleapiclient/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import functools
import inspect
import logging
import re
import urllib.parse

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -205,3 +206,25 @@ def _add_query_parameter(url, name, value):
return url
else:
return update_query_params(url, {name: value})


# The developer API key travels on every request as the ``key`` query
# parameter. It is a credential and must not be written to logs, even at
# DEBUG level (CWE-532).
_SENSITIVE_QUERY_PARAM_RE = re.compile(r"([?&]key=)[^&#]*", re.IGNORECASE)


def _redact_sensitive_url(url):
"""Redact credential query parameters from a URL before logging it.

Args:
url: string, the request URL that may carry an API key.

Returns:
A copy of the URL with the ``key`` query parameter value replaced by
``REDACTED``. Non-string input is returned unchanged.
"""
try:
return _SENSITIVE_QUERY_PARAM_RE.sub(r"\1REDACTED", url)
except TypeError:
return url
17 changes: 13 additions & 4 deletions googleapiclient/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@

# Local imports
from googleapiclient import _auth, mimeparse
from googleapiclient._helpers import _add_query_parameter, positional
from googleapiclient._helpers import (
_add_query_parameter,
_redact_sensitive_url,
positional,
)
from googleapiclient.errors import (
HttpError,
InvalidJsonError,
Expand Down Expand Up @@ -432,7 +436,7 @@ def _retrieve_discovery_doc(
actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"])
if developerKey:
actual_url = _add_query_parameter(url, "key", developerKey)
logger.debug("URL being requested: GET %s", actual_url)
logger.debug("URL being requested: GET %s", _redact_sensitive_url(actual_url))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling _redact_sensitive_url and performing string interpolation on every discovery document retrieval even when DEBUG logging is disabled introduces unnecessary CPU overhead. Wrapping the log statement in an if logger.isEnabledFor(logging.DEBUG): block avoids this overhead entirely. Additionally, per the general rules, we should prefer using f-strings for string interpolation.

Suggested change
logger.debug("URL being requested: GET %s", _redact_sensitive_url(actual_url))
if logger.isEnabledFor(logging.DEBUG):
logger.debug(f"URL being requested: GET {_redact_sensitive_url(actual_url)}")
References
  1. Prefer using f-strings for string interpolation in Python.


# Execute this request with retries build into HttpRequest
# Note that it will already raise an error if we don't get a 2xx response
Expand Down Expand Up @@ -1263,7 +1267,9 @@ def method(self, **kwargs):
) % multipart_boundary
url = _add_query_parameter(url, "uploadType", "multipart")

logger.debug("URL being requested: %s %s" % (httpMethod, url))
logger.debug(
"URL being requested: %s %s" % (httpMethod, _redact_sensitive_url(url))
)
Comment on lines +1270 to +1272

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling _redact_sensitive_url (which performs regex operations) and performing string interpolation on every single API request even when DEBUG logging is disabled introduces significant unnecessary CPU overhead on a hot path. Wrapping the log statement in an if logger.isEnabledFor(logging.DEBUG): block avoids this overhead entirely. Additionally, per the general rules, we should prefer using f-strings for string interpolation.

        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(f"URL being requested: {httpMethod} {_redact_sensitive_url(url)}")
References
  1. Prefer using f-strings for string interpolation in Python.

return self._requestBuilder(
self._http,
model.response,
Expand Down Expand Up @@ -1383,7 +1389,10 @@ def methodNext(self, previous_request, previous_response):
request.uri = _add_query_parameter(
request.uri, pageTokenName, nextPageToken
)
logger.debug("Next page request URL: %s %s" % (methodName, request.uri))
logger.debug(
"Next page request URL: %s %s"
% (methodName, _redact_sensitive_url(request.uri))
)
Comment on lines +1392 to +1395

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling _redact_sensitive_url and performing string interpolation on every pagination request even when DEBUG logging is disabled introduces unnecessary CPU overhead on a hot path. Wrapping the log statement in an if logger.isEnabledFor(logging.DEBUG): block avoids this overhead entirely. Additionally, per the general rules, we should prefer using f-strings for string interpolation.

            if logger.isEnabledFor(logging.DEBUG):
                logger.debug(f"Next page request URL: {methodName} {_redact_sensitive_url(request.uri)}")
References
  1. Prefer using f-strings for string interpolation in Python.

else:
# Replace pageToken value in request body
model = self._model
Expand Down
9 changes: 7 additions & 2 deletions googleapiclient/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def _retry_request(
num_retries,
req_type,
method,
uri,
util._redact_sensitive_url(uri),
resp.status if resp else exception,
)
sleep(sleep_time)
Expand Down Expand Up @@ -1157,7 +1157,12 @@ def next_chunk(self, http=None, num_retries=0):
self._sleep(self._rand() * 2**retry_num)
LOGGER.warning(
"Retry #%d for media upload: %s %s, following status: %d"
% (retry_num, self.method, self.uri, resp.status)
% (
retry_num,
self.method,
util._redact_sensitive_url(self.uri),
resp.status,
)
)

try:
Expand Down
41 changes: 41 additions & 0 deletions tests/test__helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,44 @@ def test_with_repeats(self):
content = "a=b&a=d"
with self.assertRaises(ValueError):
_helpers.parse_unique_urlencoded(content)


class Test_redact_sensitive_url(unittest.TestCase):
def test_redacts_key(self):
url = "https://www.googleapis.com/foo?key=SECRET&alt=json"
self.assertEqual(
_helpers._redact_sensitive_url(url),
"https://www.googleapis.com/foo?key=REDACTED&alt=json",
)

def test_redacts_key_at_end(self):
url = "https://www.googleapis.com/foo?alt=json&key=SECRET"
self.assertEqual(
_helpers._redact_sensitive_url(url),
"https://www.googleapis.com/foo?alt=json&key=REDACTED",
)

def test_redacts_key_before_fragment(self):
url = "https://www.googleapis.com/foo?key=SECRET#frag"
self.assertEqual(
_helpers._redact_sensitive_url(url),
"https://www.googleapis.com/foo?key=REDACTED#frag",
)

def test_case_insensitive(self):
url = "https://www.googleapis.com/foo?Key=SECRET"
self.assertEqual(
_helpers._redact_sensitive_url(url),
"https://www.googleapis.com/foo?Key=REDACTED",
)

def test_does_not_touch_similar_param_names(self):
url = "https://www.googleapis.com/foo?apikey=KEEP&key2=KEEP"
self.assertEqual(_helpers._redact_sensitive_url(url), url)

def test_no_key(self):
url = "https://www.googleapis.com/foo?alt=json"
self.assertEqual(_helpers._redact_sensitive_url(url), url)

def test_non_string_returned_unchanged(self):
self.assertIsNone(_helpers._redact_sensitive_url(None))
16 changes: 16 additions & 0 deletions tests/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,22 @@ def test_building_with_developer_key_skips_adc(self):
# application default credentials were used.
self.assertNotIsInstance(plus._http, google_auth_httplib2.AuthorizedHttp)

def test_developer_key_not_logged(self):
discovery = read_datafile("plus.json")
plus = build_from_document(
discovery,
base="https://www.googleapis.com/",
developerKey="SECRET_DEVELOPER_KEY",
)
with self.assertLogs("googleapiclient.discovery", level="DEBUG") as cm:
request = plus.activities().list(collection="public", userId="me")
logged = "\n".join(cm.output)
self.assertIn("URL being requested", logged)
self.assertNotIn("SECRET_DEVELOPER_KEY", logged)
self.assertIn("key=REDACTED", logged)
# The real request still carries the key; only the log line is redacted.
self.assertIn("key=SECRET_DEVELOPER_KEY", request.uri)

def test_building_with_context_manager(self):
discovery = read_datafile("plus.json")
with mock.patch("httplib2.Http") as http:
Expand Down
Loading