Skip to content

Conversation

drnic
Copy link

@drnic drnic commented Jul 21, 2025

Faraday supports logging of responses but annoyingly this can only be configured at the time of constructing the Faraday.new instance, and Adyen::Client locks this initialization inside call_adyen_api method.

This PR adds new logger: argument and #logger= setter to Adyen::Client.

Example usage:

client = Adyen::Client.new
client.logger = Logger.new($stdout)

or

client = Adyen::Client.new(logger: Logger.new($stdout))

Output in logs might look like:

I, [2025-07-21T06:20:24.131342 #45006]  INFO -- : request: POST https://checkout-test.adyen.com/v71/payments
I, [2025-07-21T06:20:24.131376 #45006]  INFO -- : request: Content-Type: "application/json"
User-Agent: "adyen-ruby-api-library/10.3.0"
x-api-key: "l123AQEuhmfxL4..."
adyen-library-name: "adyen-ruby-api-library"
adyen-library-version: "10.3.0"
I, [2025-07-21T06:20:24.131403 #45006]  INFO -- : request: {"reference":"xyz","paymentMethod":{"type":"scheme","checkoutAttemptId":"0849f4a5","encryptedCardNumber":"eyJhbGciOi...
I, [2025-07-21T06:20:25.564473 #45006]  INFO -- : response: Status 200
I, [2025-07-21T06:20:25.564604 #45006]  INFO -- : response: traceparent: "00-
...
transfer-encoding: "chunked"
date: "Mon, 21 Jul 2025 06:20:25 GMT"
strict-transport-security: "max-age=31536000; includeSubDomains"
I, [2025-07-21T06:20:25.564697 #45006]  INFO -- : response: {"additionalData":{"cardSummary":"0008","isCardCommercial":"unknown","authorisationMid":"50","...

@drnic drnic requested a review from a team as a code owner July 21, 2025 06:25
@gcatanese
Copy link
Contributor

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new logger: argument and #logger= setter to Adyen::Client. The addition of a logger is valuable for debugging, but the current implementation of logging request and response bodies introduces a critical security vulnerability by potentially logging sensitive PCI data (like card numbers and CVCs) in plain text. The review includes a suggested code change to implement filtering for this sensitive data, ensuring the library remains secure by default.

Comment on lines 168 to 170
if @logger
faraday.response :logger, @logger, bodies: true
end

Choose a reason for hiding this comment

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

critical

Enabling body logging with bodies: true introduces a critical security risk. It will log raw request and response bodies, which can contain sensitive PCI data like card numbers (number) and security codes (cvc). This data should never be stored in logs.

To mitigate this, the logger should filter out sensitive information before writing to the log. I suggest wrapping the provided logger in a decorator that filters the log messages before they are processed. This approach ensures that even if a plain logger is passed in, the library takes responsibility for filtering sensitive data, making it secure by default.

Here is a potential implementation using SimpleDelegator from Ruby's standard library. This wrapper will intercept log messages and apply a regex to redact sensitive fields, based on the fields already identified as sensitive in Adyen::AdyenError.

require 'delegate' unless defined?(SimpleDelegator)

if @logger
  # A logger wrapper that filters sensitive data from log messages.
  # This prevents accidental logging of card numbers, CVCs, etc.
  filtering_logger_class = Class.new(SimpleDelegator) do
    SENSITIVE_FIELDS = %w[
      number cvc expiryMonth expiryYear encryptedCardNumber
      encryptedExpiryMonth encryptedExpiryYear encryptedSecurityCode
    ].freeze
    # Regex to find sensitive fields in a JSON string and replace their values.
    FILTER_REGEX = /"(#{SENSITIVE_FIELDS.join('|')})":\s*".*?"/i.freeze

    def info(msg)
      __getobj__.info(filter(msg))
    end

    def debug(msg)
      __getobj__.debug(filter(msg))
    end

    private

    def filter(msg)
      msg.is_a?(String) ? msg.gsub(FILTER_REGEX, '"\1":"***"') : msg
    end
  end

  faraday.response :logger, filtering_logger_class.new(@logger), bodies: true
end

@gcatanese
Copy link
Contributor

@drnic thank you very much for your contribution! 🙌
I understand the reason behind this improvement, however logging response bodies might log/expose
sensitive PCI data, therefore it is something we prefer not including in the library.
The alternatives could be:

  • log the response body on need-basis, within your integration. You have then full control or what and when gets logged.
  • extend the library to allow injecting a custom Faraday handler that can be configured with all settings required (logger, timeouts, etc..): in this case your integration would be responsible to define and inject the Faraday instance

I strongly recommend going with the first option. We can consider option two in a future release if needed.

Logging is helpful for troubleshooting and observability, but it comes with a high risk of exposing sensitive data. Be mindful of what is logged, keep it to the minimum necessary, and make sure logs are properly secured and not accessible or misused, especially in a LIVE environment.
Thank you again for raising this aspect.

Copy link

@drnic
Copy link
Author

drnic commented Jul 25, 2025

@gcatanese good call, I've dropped the , bodies: true from the PR. We can consider more flexible logging in future. I'm about to go on vacation, but didn't want to leave the PR unloved.

@drnic
Copy link
Author

drnic commented Jul 25, 2025

If anyone's seen some "how to sanitize payment response bodies" code we can use, point me to it and I'll create a new PR in a few weeks when I'm back.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants