Skip to content
Draft
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
13 changes: 6 additions & 7 deletions src/firebase_functions/private/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,14 @@ def _on_call_valid_body(request: _Request) -> bool:

# The body must have data.
if "data" not in request.json:
_logging.warning("Request body is missing data.", request.json)
_logging.warning("Request body is missing data.")
return False

extra_keys = {key: request.json[key] for key in request.json.keys() if key != "data"}
if len(extra_keys) != 0:
_logging.warning(
"Request body has extra fields: %s",
"".join(f"{key}: {value}," for (key, value) in extra_keys.items()),
)
# Logged before auth is checked: key names only, never values. %r escapes
# newlines so a key cannot forge extra log lines.
extra_keys = [key for key in request.json.keys() if key != "data"]
if extra_keys:
_logging.warning("Request body has extra fields: %r", extra_keys)
return False
Comment on lines 123 to 132

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If the request body is a JSON list (e.g., ["data", "extra"]) or a non-dictionary type (e.g., an integer or string), checking "data" not in request.json or calling request.json.keys() will raise a TypeError or AttributeError. Checking isinstance(request.json, dict) first ensures that the request body is a dictionary before accessing its keys or checking for the presence of "data" keys.

Suggested change
if "data" not in request.json:
_logging.warning("Request body is missing data.", request.json)
_logging.warning("Request body is missing data.")
return False
extra_keys = {key: request.json[key] for key in request.json.keys() if key != "data"}
if len(extra_keys) != 0:
_logging.warning(
"Request body has extra fields: %s",
"".join(f"{key}: {value}," for (key, value) in extra_keys.items()),
)
# Request content is logged before auth is checked, so log neither keys nor values.
extra_field_count = sum(1 for key in request.json.keys() if key != "data")
if extra_field_count:
_logging.warning("Request body has extra fields (count=%d).", extra_field_count)
return False
if not isinstance(request.json, dict) or "data" not in request.json:
_logging.warning("Request body is missing data.")
return False
# Request content is logged before auth is checked, so log neither keys nor values.
extra_field_count = sum(1 for key in request.json.keys() if key != "data")
if extra_field_count:
_logging.warning("Request body has extra fields (count=%d).", extra_field_count)
return False

return True

Expand Down
33 changes: 33 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
"""

import datetime as _dt
import logging
from os import environ, path

from flask import Flask

from firebase_functions.private.util import (
_on_call_valid_body,
_unsafe_decode_id_token,
deep_merge,
firebase_config,
Expand Down Expand Up @@ -165,3 +169,32 @@ def test_unsafe_decode_token():
result = _unsafe_decode_id_token(test_token)
assert result["sub"] == "firebase"
assert result["name"] == "John Doe"


def _valid_body(body: str) -> bool:
with Flask(__name__).test_request_context(
method="POST", data=body, content_type="application/json"
) as ctx:
return _on_call_valid_body(ctx.request)


def test_on_call_valid_body_logs_extra_field_keys_without_values(caplog):
with caplog.at_level(logging.WARNING):
assert not _valid_body('{"data": {}, "extraKey": "secretValue"}')
assert "Request body has extra fields: ['extraKey']" in caplog.text
assert "secretValue" not in caplog.text


def test_on_call_valid_body_escapes_newlines_in_extra_field_keys(caplog):
with caplog.at_level(logging.WARNING):
assert not _valid_body('{"data": {}, "a\\nFAKE LOG LINE": 1}')
assert "\n" not in caplog.text.rstrip("\n")


def test_on_call_valid_body_does_not_log_body_when_data_is_missing(caplog):
for body in ('{"secretKey": "secretValue"}', '["secretValue"]'):
caplog.clear()
with caplog.at_level(logging.WARNING):
assert not _valid_body(body)
assert "Request body is missing data." in caplog.text
assert "secretValue" not in caplog.text
Comment on lines +194 to +200

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Add additional test cases to verify that non-dictionary JSON bodies (such as lists containing the string "data" or raw integers) are handled gracefully without raising unhandled exceptions.

Suggested change
def test_on_call_valid_body_does_not_log_body_when_data_is_missing(caplog):
for body in ('{"secretKey": "secretValue"}', '["secretValue"]'):
caplog.clear()
with caplog.at_level(logging.WARNING):
assert not _valid_body(body)
assert "Request body is missing data." in caplog.text
assert "secretValue" not in caplog.text
def test_on_call_valid_body_does_not_log_body_when_data_is_missing(caplog):
for body in ('{"secretKey": "secretValue"}', '["secretValue"]', '["data", "secretValue"]', '123'):
caplog.clear()
with caplog.at_level(logging.WARNING):
assert not _valid_body(body)
assert "Request body is missing data." in caplog.text
assert "secretValue" not in caplog.text

Loading