diff --git a/solarwinds_apm/oboe/trace_options.py b/solarwinds_apm/oboe/trace_options.py index 0c0e5e1a5..3ead6f671 100644 --- a/solarwinds_apm/oboe/trace_options.py +++ b/solarwinds_apm/oboe/trace_options.py @@ -382,6 +382,10 @@ def validate_signature(header, signature, key, timestamp): "Failed to encode key or header for signature validation: %s", exc ) return Auth.BAD_SIGNATURE - if signature == digest: + try: + signature_matches = hmac.compare_digest(signature, digest) + except (TypeError, ValueError): + return Auth.BAD_SIGNATURE + if signature_matches: return Auth.OK return Auth.BAD_SIGNATURE diff --git a/tests/unit/test_oboe/test_trace_options.py b/tests/unit/test_oboe/test_trace_options.py index 7422a9046..714479158 100644 --- a/tests/unit/test_oboe/test_trace_options.py +++ b/tests/unit/test_oboe/test_trace_options.py @@ -351,3 +351,26 @@ def test_stringify_trace_options_response(): def test_validate_signature(header, signature, key, timestamp, expected): result = validate_signature(header, signature, key, timestamp) assert result == expected + + +def test_validate_signature_uses_constant_time_comparison(mocker): + compare_digest = mocker.patch( + "solarwinds_apm.oboe.trace_options.hmac.compare_digest", + return_value=True, + ) + + result = validate_signature("header", "signature", "key", int(time.time())) + + compare_digest.assert_called_once() + assert result == Auth.OK + + +@pytest.mark.parametrize( + "signature", ["", "not-a-digest", "é", None, b"signature", 123] +) +def test_validate_signature_rejects_malformed_signatures( + signature: object, +) -> None: + """Reject malformed signatures without propagating comparison errors.""" + result = validate_signature("header", signature, "key", int(time.time())) + assert result == Auth.BAD_SIGNATURE