CVE-2022-4823: Observable Discrepancy (CWE-203)
value: A vulnerability, which was classified as problematic, was found in InSTEDD Nuntium. Affected is an unknown function of the file app/controllers/geopoll_controller.rb. The manipulation of the argument signature leads to observable timing discrepancy. It is possible to launch the attack remotely. The name of the patch is 77236f7fd71a0e2eefeea07f9866b069d612cf0d. It is recommended to apply a patch to fix this issue. VDB-217002 is the identifier assigned to this vulnerability.
CWE description: The product behaves differently or sends different responses under different circumstances in a way that is observable to an unauthorized actor, which exposes security-relevant information about the state of the product, such as whether a particular operation was successful or not.
CVSS v3 base score: 5.9
Published: 2022-12-28T21:15Z
Repository: https://github.com/instedd/nuntium
Language: Ruby
Files changed: app/controllers/geopoll_controller.rb, app/views/interactions/show.html.erb
Commit date: 2022-11-23 19:35:07 -0300
Commit message: Detect GeoPoll AT's to: number (#88)

* Securely check GeoPoll incoming signature

This avoids timing attacks

* Log unknown GeoPoll requests parameters

To avoid losing data upon API changes

See #76

* Show GeoPoll channels in Interactions tab

* Detect GeoPoll AT's to: numbers

Fixes #76
Diff: diff --git a/app/controllers/geopoll_controller.rb b/app/controllers/geopoll_controller.rb
index 9e75efe6..8c18e13c 100644
--- a/app/controllers/geopoll_controller.rb
+++ b/app/controllers/geopoll_controller.rb
@@ -18,29 +18,36 @@
 require 'digest/md5'
 
 class GeopollController < ApplicationController
   skip_filter :check_login
 
-  # POST /:account_name/:channel_name/:secret_token/geopoll/incoming
+  # POST /:account_name/:channel_name/geopoll/incoming
   def incoming
     account = Account.find_by_id_or_name(params[:account_name])
     channel = account.geopoll_channels.find_by_name(params[:channel_name])
     auth_token = channel.configuration[:auth_token].to_s.split(' ')[1]
     identifier = params[:Identifier]
     signature = Digest::MD5.hexdigest(auth_token + identifier)
 
-    if signature != params[:Signature]
+    if !(ActiveSupport::SecurityUtils.secure_compare params[:Signature], signature)
       return render text: "Error", status: :unauthorized
     end
 
+    unknown_params = params.except(
+      'Identifier', 'Signature', 'SourceAddress', 'TargetAddress', 'MessageText', # GeoPoll API specification
+      'account_name', 'channel_name', 'controller', 'action' # Rails-generated parameters
+    )
+
     msg = AtMessage.new
     msg.from = "sms://#{params[:SourceAddress]}"
-    msg.to   = "sms://#{channel.configuration[:from]}"
+    msg.to   = "sms://#{params[:TargetAddress]}"
     msg.body = params[:MessageText]
     msg.channel_relative_id = params[:Identifier]
     account.route_at msg, channel
 
+    channel.logger.warning :channel_id => channel.id, :at_message_id => msg.id, :message => "Received unknown parameters for AT #{msg.id}: #{unknown_params.to_json}" unless unknown_params.empty?
+    
     render text: "Accepted"
   end
 
   def status
     account = Account.find_by_id_or_name(params[:account_name])
@@ -49,10 +56,15 @@ class GeopollController < ApplicationController
 
     unless ao
       return render text: "Error", status: :not_found
     end
 
+    unknown_params = params.except(
+      'MessageId', 'Status', # GeoPoll API specification
+      'account_name', 'channel_name', 'controller', 'action' # Rails-generated parameters
+    )
+
     status = params[:Status]
     case status
       when "SUCCESS"
         ao.state = "confirmed"
       when "UNDELIVERABLE_TO_MESSAGING_PROVIDER", "REJECTED_BY_MESSAGING_PROVIDER",
@@ -61,12 +73,14 @@ class GeopollController < ApplicationController
     end
 
     account.logger.info :channel_id => channel.id, :ao_message_id => ao.id,
       :message => "Recieved delivery notification with status #{status.inspect}"
 
-     ao.custom_attributes[:geopoll_status] = status if status
-     ao.save!
+    channel.logger.warning :channel_id => channel.id, :at_message_id => msg.id, :message => "Received unknown parameters for AO #{msg.id} Status: #{unknown_params.to_json}" unless unknown_params.empty?
+
+    ao.custom_attributes[:geopoll_status] = status if status
+    ao.save!
 
     render text: "Accepted"
   end
 
   def balance
diff --git a/app/views/interactions/show.html.erb b/app/views/interactions/show.html.erb
index f2fb482d..33b9689d 100644
--- a/app/views/interactions/show.html.erb
+++ b/app/views/interactions/show.html.erb
@@ -108,10 +108,22 @@
           create Application Terminated Messages in channel <%= channel.name %></li>
       <%- end -%>
     </ul>
   <%- end -%>
 
+  <%- if channels.any?{|c| c.kind == 'geopoll'} -%>
+    <div><b>Via <%= link_to 'GeoPoll', 'https://www.geopoll.com/', :target => '_blank' -%></b></div>
+    <ul>
+      <%- channels.select{|c| c.kind == 'geopoll'}.each do |channel| -%>
+        <li>Incoming Messages URL: <span class="url">POST <%= geopoll_incoming_url(:account_name => account.name, :channel_name => channel.name) -%></span>
+          create Application Terminated Messages in channel <%= channel.name %></li>
+        <li>Delivery Reports Receiver URL: <span class="url">POST <%= geopoll_status_url(:account_name => account.name, :channel_name => channel.name) -%></span>
+          receive delivery reports in channel <%= channel.name %></li>
+      <%- end -%>
+    </ul>
+  <%- end -%>
+
   <%- unless applications.empty? -%>
     <div><b>Via <%= link_to 'RSS', 'http://en.wikipedia.org/wiki/RSS', :target => '_blank' -%></b></div>
     Use <%= link_to 'Http Basic Authentication', 'http://en.wikipedia.org/wiki/Basic_access_authentication', :target => '_blank' -%> with application credentials (use "account/application" as username).
 
     <ul>

Vulnerable code:   # POST /:account_name/:channel_name/:secret_token/geopoll/incoming
    if signature != params[:Signature]
    msg.to   = "sms://#{channel.configuration[:from]}"
     ao.custom_attributes[:geopoll_status] = status if status
     ao.save!
Fixed code:   # POST /:account_name/:channel_name/geopoll/incoming
    if !(ActiveSupport::SecurityUtils.secure_compare params[:Signature], signature)
    unknown_params = params.except(
      'Identifier', 'Signature', 'SourceAddress', 'TargetAddress', 'MessageText', # GeoPoll API specification
      'account_name', 'channel_name', 'controller', 'action' # Rails-generated parameters
    )

    msg.to   = "sms://#{params[:TargetAddress]}"
    channel.logger.warning :channel_id => channel.id, :at_message_id => msg.id, :message => "Received unknown parameters for AT #{msg.id}: #{unknown_params.to_json}" unless unknown_params.empty?
    
    unknown_params = params.except(
      'MessageId', 'Status', # GeoPoll API specification
      'account_name', 'channel_name', 'controller', 'action' # Rails-generated parameters
    )

    channel.logger.warning :channel_id => channel.id, :at_message_id => msg.id, :message => "Received unknown parameters for AO #{msg.id} Status: #{unknown_params.to_json}" unless unknown_params.empty?

    ao.custom_attributes[:geopoll_status] = status if status
    ao.save!
  <%- if channels.any?{|c| c.kind == 'geopoll'} -%>
    <div><b>Via <%= link_to 'GeoPoll', 'https://www.geopoll.com/', :target => '_blank' -%></b></div>
    <ul>
      <%- channels.select{|c| c.kind == 'geopoll'}.each do |channel| -%>
        <li>Incoming Messages URL: <span class="url">POST <%= geopoll_incoming_url(:account_name => account.name, :channel_name => channel.name) -%></span>
          create Application Terminated Messages in channel <%= channel.name %></li>
        <li>Delivery Reports Receiver URL: <span class="url">POST <%= geopoll_status_url(:account_name => account.name, :channel_name => channel.name) -%></span>
          receive delivery reports in channel <%= channel.name %></li>
      <%- end -%>
    </ul>
  <%- end -%>