Skip to content
Open
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
4 changes: 2 additions & 2 deletions lib/aikido/zen/attack_wave.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ def initialize(config: Aikido::Zen.config, clock: nil)
end
end

def attack_wave?(context)
def attack_wave?(context, status_code = nil)
client_ip = context.request.client_ip

return false unless client_ip

return false if @event_times[client_ip]

return false unless AttackWave::Helpers.web_scanner?(context)
return false unless AttackWave::Helpers.web_scanner?(context, status_code)

request_count = @request_counts[client_ip] += 1

Expand Down
19 changes: 14 additions & 5 deletions lib/aikido/zen/attack_wave/helpers.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
# frozen_string_literal: true

require "set"

module Aikido::Zen
module AttackWave
module Helpers
def self.web_scanner?(context)
return true if suspicious_request?(context)
def self.web_scanner?(context, status_code)
return true if suspicious_request?(context, status_code)

return true if include_suspicious_payload?(context)

false
end

def self.suspicious_request?(context)
def self.suspicious_request?(context, status_code)
request = context.request

suspicious_method?(request.request_method) || suspicious_path?(request.path_info)
suspicious_method?(request.request_method) || suspicious_path?(request.path_info, status_code)
end

def self.suspicious_method?(method)
SUSPICIOUS_METHODS.include?(method.downcase)
end

def self.suspicious_path?(path)
def self.suspicious_path?(path, status_code)
path_parts = path.downcase.split("/")

file_name = path_parts.pop if path_parts.length > 0
Expand All @@ -34,6 +36,8 @@ def self.suspicious_path?(path)
file_extension = file_name_parts.pop if file_name_parts.length > 1

return true if SUSPICIOUS_FILE_EXTENSIONS.include?(file_extension)

return true if FOREIGN_EXTENSIONS.include?(file_extension) && status_code == 404

@aikido-pr-checks aikido-pr-checks Bot Jun 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Avoid a chained condition in the nested file_name branch. Split FOREIGN_EXTENSIONS check and status_code check into clearer guard/early-return steps to reduce nesting and improve readability.

Suggested change
return true if FOREIGN_EXTENSIONS.include?(file_extension) && status_code == 404
if FOREIGN_EXTENSIONS.include?(file_extension)
return true if status_code == 404
end
Details

✨ AI Reasoning
​The change inserted a new return that combines two checks with && inside the nested file_name branch, increasing branching complexity in a block that already contains multiple return-true checks. Separating these into explicit guard checks (e.g., check file_extension membership first, then check status_code) or inverting the condition into an early-return/guard would flatten nesting and improve readability and maintainability.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

end

path_parts.any? do |directory_name|
Expand Down Expand Up @@ -434,6 +438,11 @@ def self.include_suspicious_payload?(context)
"sqlite3db"
].map(&:downcase).freeze

# Extensions that a Ruby app would not natively serve. Requests to these
# paths are only treated as scan hits when the response is 404 — a 200
# may indicate the app is proxying to a PHP/Java backend.
FOREIGN_EXTENSIONS = Set.new(%w[php php3 php4 php5 phtml java jsp jspx]).freeze

SUSPICIOUS_SQL_KEYWORDS = [
"SELECT (CASE WHEN",
"SELECT COUNT(",
Expand Down
11 changes: 6 additions & 5 deletions lib/aikido/zen/middleware/attack_wave_protector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,29 @@ def initialize(app, zen: Aikido::Zen, settings: Aikido::Zen.runtime_settings)

def call(env)
response = @app.call(env)
status_code = response[0].to_i

context = @zen.current_context
protect(context)
protect(context, status_code)

response
end

# @api private
# Visible for testing.
def attack_wave?(context)
def attack_wave?(context, status_code = nil)
request = context.request
return false if request.nil?

return false if @settings.bypassed_ips.include?(request.client_ip)

@zen.attack_wave_detector.attack_wave?(context)
@zen.attack_wave_detector.attack_wave?(context, status_code)
end

# @api private
# Visible for testing.
def protect(context)
if attack_wave?(context)
def protect(context, status_code = nil)
if attack_wave?(context, status_code)
client_ip = context.request.client_ip

request = Aikido::Zen::AttackWave::Request.new(
Expand Down
32 changes: 32 additions & 0 deletions test/aikido/zen/attack_wave_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,38 @@
require "test_helper"

class Aikido::Zen::AttackWaveTest < ActiveSupport::TestCase
class HelpersTest < ActiveSupport::TestCase
test "suspicious_path? returns true for foreign extension with 404 status" do
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/admin.php", 404)
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/app.jsp", 404)
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/page.jspx", 404)
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/index.php3", 404)
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/index.php4", 404)
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/index.php5", 404)
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/index.phtml", 404)
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/Hello.java", 404)
end

test "suspicious_path? returns false for foreign extension with non-404 status" do
refute Aikido::Zen::AttackWave::Helpers.suspicious_path?("/admin.php", 200)
refute Aikido::Zen::AttackWave::Helpers.suspicious_path?("/app.jsp", 200)
refute Aikido::Zen::AttackWave::Helpers.suspicious_path?("/admin.php", 301)
refute Aikido::Zen::AttackWave::Helpers.suspicious_path?("/admin.php", 500)
refute Aikido::Zen::AttackWave::Helpers.suspicious_path?("/admin.php", nil)
end

test "suspicious_path? still returns true for always-suspicious extensions regardless of status" do
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/backup.sql", 200)
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/data.db", 200)
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/dump.bak", 200)
end

test "suspicious_path? still returns true for suspicious file names regardless of status" do
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/.gitconfig", 200)
assert Aikido::Zen::AttackWave::Helpers.suspicious_path?("/wp-config.php", 200)
end
end

class TestClock
attr_reader :at

Expand Down
Loading