File: controller_ext.rb

package info (click to toggle)
ruby-invisible-captcha 2.3.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 384 kB
  • sloc: ruby: 717; makefile: 6
file content (132 lines) | stat: -rw-r--r-- 3,807 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# frozen_string_literal: true

module InvisibleCaptcha
  module ControllerExt
    module ClassMethods
      def invisible_captcha(options = {})
        if options.key?(:prepend)
          prepend_before_action(options) do
            detect_spam(options)
          end
        else
          before_action(options) do
            detect_spam(options)
          end
        end
      end
    end

    private

    def detect_spam(options = {})
      if timestamp_spam?(options)
        on_timestamp_spam(options)
        return if performed?
      end

      if honeypot_spam?(options) || spinner_spam?
        on_spam(options)
      end
    end

    def on_timestamp_spam(options = {})
      if action = options[:on_timestamp_spam]
        send(action)
      else
        flash[:error] = InvisibleCaptcha.timestamp_error_message
        redirect_back(fallback_location: root_path)
      end
    end

    def on_spam(options = {})
      if action = options[:on_spam]
        send(action)
      else
        head(200)
      end
    end

    def timestamp_spam?(options = {})
      enabled = if options.key?(:timestamp_enabled)
        options[:timestamp_enabled]
      else
        InvisibleCaptcha.timestamp_enabled
      end

      return false unless enabled

      timestamp = session.delete(:invisible_captcha_timestamp)

      # Consider as spam if timestamp not in session, cause that means the form was not fetched at all
      unless timestamp
        warn_spam("Timestamp not found in session.")
        return true
      end

      time_to_submit = Time.zone.now - DateTime.iso8601(timestamp)
      threshold = options[:timestamp_threshold] || InvisibleCaptcha.timestamp_threshold

      # Consider as spam if form submitted too quickly
      if time_to_submit < threshold
        warn_spam("Timestamp threshold not reached (took #{time_to_submit.to_i}s).")
        return true
      end

      false
    end

    def spinner_spam?
      if InvisibleCaptcha.spinner_enabled && (params[:spinner].blank? || params[:spinner] != session[:invisible_captcha_spinner])
        warn_spam("Spinner value mismatch")
        return true
      end

      false
    end

    def honeypot_spam?(options = {})
      honeypot = options[:honeypot]
      scope    = options[:scope] || controller_name.singularize

      if honeypot
        # If honeypot is defined for this controller-action, search for:
        # - honeypot: params[:subtitle]
        # - honeypot with scope: params[:topic][:subtitle]
        if params[honeypot].present? || (params[scope] && params[scope][honeypot].present?)
          warn_spam("Honeypot param '#{honeypot}' was present.")
          return true
        else
          # No honeypot spam detected, remove honeypot from params to avoid UnpermittedParameters exceptions
          params.delete(honeypot) if params.key?(honeypot)
          params[scope].try(:delete, honeypot) if params.key?(scope)
        end
      else
        InvisibleCaptcha.honeypots.each do |default_honeypot|
          if params[default_honeypot].present? || (params[scope] && params[scope][default_honeypot].present?)
            warn_spam("Honeypot param '#{scope}.#{default_honeypot}' was present.")
            return true
          end
        end
      end

      false
    end

    def warn_spam(message)
      message = "[Invisible Captcha] Potential spam detected for IP #{request.remote_ip}. #{message}"

      logger.warn(message)

      ActiveSupport::Notifications.instrument(
        'invisible_captcha.spam_detected',
        message: message,
        remote_ip: request.remote_ip,
        user_agent: request.user_agent,
        controller: params[:controller],
        action: params[:action],
        url: request.url,
        params: request.filtered_parameters
      )
    end
  end
end