Skip to content
Draft
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
2 changes: 2 additions & 0 deletions lib/aikido/zen/sinks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
require_relative "sinks/kernel"

require_relative "sinks/file"
require_relative "sinks/io"
require_relative "sinks/pathname"
require_relative "sinks/socket"
require_relative "sinks/resolv"

Expand Down
4 changes: 4 additions & 0 deletions lib/aikido/zen/sinks/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ def join(*args, **kwargs, &blk)
Helpers.scan(file_name, "expand_path")
end

sink_before :absolute_path do |file_name|
Helpers.scan(file_name, "absolute_path")
end

sink_before :realpath do |file_name|
Helpers.scan(file_name, "realpath")
end
Expand Down
50 changes: 50 additions & 0 deletions lib/aikido/zen/sinks/io.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

module Aikido::Zen
module Sinks
module IO
SINK = Sinks.add("IO", scanners: [Scanners::PathTraversalScanner])

module Helpers
def self.scan(filepath, operation)
SINK.scan(
filepath: filepath,
operation: operation
)
end
end

def self.load_sinks!
::IO.singleton_class.class_eval do
extend Sinks::DSL

sink_before :read do |path, *|
Helpers.scan(path, "read")
end

sink_before :write do |path, *|
Helpers.scan(path, "write")
end

sink_before :foreach do |path, *|
Helpers.scan(path, "foreach")
end

sink_before :readlines do |path, *|
Helpers.scan(path, "readlines")
end

sink_before :binread do |path, *|
Helpers.scan(path, "binread")
end

sink_before :binwrite do |path, *|
Helpers.scan(path, "binwrite")
end
end
end
end
end
end

Aikido::Zen::Sinks::IO.load_sinks!
32 changes: 32 additions & 0 deletions lib/aikido/zen/sinks/pathname.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

require "pathname"

module Aikido::Zen
module Sinks
module Pathname
SINK = Sinks.add("Pathname", scanners: [Scanners::PathTraversalScanner])

module Helpers
def self.scan(filepath, operation)
SINK.scan(
filepath: filepath,
operation: operation
)
end
end

def self.load_sinks!
::Pathname.class_eval do
extend Sinks::DSL

sink_before :cleanpath do
Aikido::Zen::Sinks::Pathname::Helpers.scan(to_s, "cleanpath")
end
end
end
end
end
end

Aikido::Zen::Sinks::Pathname.load_sinks!
15 changes: 15 additions & 0 deletions test/aikido/zen/sinks/file_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ class NormalExecutionTest < ActiveSupport::TestCase
assert_equal "/some-path", File.expand_path("../some-path/this-wont-appear/..", "/")
end

test "File.absolute_path" do
assert File.absolute_path("../some-path", "/base").end_with?("some-path")
end

test "File.realpath" do
assert File.realpath("../../sinks", __FILE__).end_with?("test/aikido/zen/sinks")
end
Expand Down Expand Up @@ -325,6 +329,13 @@ class LookLikeAttackTest < ActiveSupport::TestCase
end
end

test "File.absolute_path" do
refute_attack do
result = File.absolute_path(LOOKS_LIKE_AN_ATTACK_PATH, __FILE__)
assert result.end_with?("looks-like-an-attack")
end
end

test "File.realpath" do
refute_attack do
assert_raise(Errno::ENOTDIR, Errno::ENOENT) do
Expand Down Expand Up @@ -429,6 +440,10 @@ def assert_path_traversal_attack(operation, &block)
File.expand_path OFFENDER_PATH
end

assert_path_traversal_attack "File.absolute_path" do
File.absolute_path OFFENDER_PATH
end

assert_path_traversal_attack "File.realpath" do
File.realpath OFFENDER_PATH
end
Expand Down
170 changes: 170 additions & 0 deletions test/aikido/zen/sinks/io_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# frozen_string_literal: true

require "test_helper"

class Aikido::Zen::Sinks::IOTest < ActiveSupport::TestCase
class NormalExecutionTest < ActiveSupport::TestCase
include StubsCurrentContext
include SinkAttackHelpers

test "IO.read" do
Helpers.temp_file do |tmp_file|
tmp_file.write "some content"
tmp_file.close
assert_equal "some content", IO.read(tmp_file.path)
end
end

test "IO.write" do
path = Helpers.temp_file_name "io-sink-write"
IO.write path, "io-sink-write"
assert_equal "io-sink-write", IO.read(path)
File.unlink path
end

test "IO.foreach" do
Helpers.temp_file do |tmp_file|
tmp_file.write "line1\nline2\n"
tmp_file.close
lines = []
IO.foreach(tmp_file.path) { |l| lines << l }
assert_equal ["line1\n", "line2\n"], lines
end
end

test "IO.readlines" do
Helpers.temp_file do |tmp_file|
tmp_file.write "line1\nline2\n"
tmp_file.close
assert_equal ["line1\n", "line2\n"], IO.readlines(tmp_file.path)
end
end

test "IO.binread" do
Helpers.temp_file do |tmp_file|
tmp_file.write "binary"
tmp_file.close
assert_equal "binary", IO.binread(tmp_file.path)
end
end

test "IO.binwrite" do
path = Helpers.temp_file_name "io-sink-binwrite"
IO.binwrite path, "binary"
assert_equal "binary", IO.binread(path)
File.unlink path
end
end

class LookLikeAttackTest < ActiveSupport::TestCase
include StubsCurrentContext
include SinkAttackHelpers

LOOKS_LIKE_AN_ATTACK_PATH = "../looks-like-an-attack"

test "IO.read" do
refute_attack do
assert_raise Errno::ENOENT do
IO.read(LOOKS_LIKE_AN_ATTACK_PATH)
end
end
end

test "IO.write" do
refute_attack do
assert_raise Errno::ENOENT do
IO.write Helpers.temp_file_name + "/" + LOOKS_LIKE_AN_ATTACK_PATH, "content"
end
end
end

test "IO.foreach" do
refute_attack do
assert_raise Errno::ENOENT do
IO.foreach(LOOKS_LIKE_AN_ATTACK_PATH) {}
end
end
end

test "IO.readlines" do
refute_attack do
assert_raise Errno::ENOENT do
IO.readlines(LOOKS_LIKE_AN_ATTACK_PATH)
end
end
end

test "IO.binread" do
refute_attack do
assert_raise Errno::ENOENT do
IO.binread(LOOKS_LIKE_AN_ATTACK_PATH)
end
end
end

test "IO.binwrite" do
refute_attack do
assert_raise Errno::ENOENT do
IO.binwrite Helpers.temp_file_name + "/" + LOOKS_LIKE_AN_ATTACK_PATH, "content"
end
end
end
end

class AttackDetectionTest < ActiveSupport::TestCase
include StubsCurrentContext
include SinkAttackHelpers

OFFENDER_PATH = "../this-is-an-attack"

def assert_path_traversal_attack(operation, &block)
set_context_from_request_to "/?filename=#{OFFENDER_PATH}"

error = assert_attack Aikido::Zen::Attacks::PathTraversalAttack, &block

assert_equal operation, error.attack.operation
end

test "attacks are detected by the scanner" do
assert_path_traversal_attack "IO.read" do
IO.read OFFENDER_PATH
end

assert_path_traversal_attack "IO.write" do
IO.write OFFENDER_PATH, "content"
end

assert_path_traversal_attack "IO.foreach" do
IO.foreach(OFFENDER_PATH) {}
end

assert_path_traversal_attack "IO.readlines" do
IO.readlines OFFENDER_PATH
end

assert_path_traversal_attack "IO.binread" do
IO.binread OFFENDER_PATH
end

assert_path_traversal_attack "IO.binwrite" do
IO.binwrite OFFENDER_PATH, "content"
end
end
end

module Helpers
def self.temp_file_name(basename = "io-sink-temp-file")
::Dir::Tmpname.create(basename, Dir.tmpdir) { |path| return path }
end

def self.temp_file(filename = "io-sink-temp-file", &block)
tmp_file = Tempfile.new filename
begin
yield tmp_file
tmp_file.close
ensure
tmp_file.unlink
end
end
end
end
75 changes: 75 additions & 0 deletions test/aikido/zen/sinks/pathname_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

require "test_helper"
require "pathname"

class Aikido::Zen::Sinks::PathnameTest < ActiveSupport::TestCase
class NormalExecutionTest < ActiveSupport::TestCase
include StubsCurrentContext
include SinkAttackHelpers

test "Pathname#cleanpath with a normal relative path" do
refute_attack do
result = Pathname.new("some/./path/../file.txt").cleanpath
assert_equal Pathname.new("some/file.txt"), result
end
end

test "Pathname#cleanpath with an absolute path" do
refute_attack do
result = Pathname.new("/var/app/data/file.txt").cleanpath
assert_equal Pathname.new("/var/app/data/file.txt"), result
end
end
end

class LookLikeAttackTest < ActiveSupport::TestCase
include StubsCurrentContext
include SinkAttackHelpers

test "Pathname#cleanpath with a traversal path but no context" do
refute_attack do
result = Pathname.new("../../../../etc/passwd").cleanpath
assert_equal Pathname.new("../../../../etc/passwd"), result
end
end
end

class AttackDetectionTest < ActiveSupport::TestCase
include StubsCurrentContext
include SinkAttackHelpers

OFFENDER_PATH = "../this-is-an-attack"

def assert_path_traversal_attack(operation, &block)
set_context_from_request_to "/?filename=#{OFFENDER_PATH}"

error = assert_attack Aikido::Zen::Attacks::PathTraversalAttack, &block

assert_equal operation, error.attack.operation
end

test "Pathname#cleanpath detects traversal before it is resolved" do
assert_path_traversal_attack "Pathname.cleanpath" do
Pathname.new(OFFENDER_PATH).cleanpath
end
end

test "detects traversal in paths joined with a base before cleanpath" do
set_context_from_request_to "/?filename=#{OFFENDER_PATH}"

assert_attack Aikido::Zen::Attacks::PathTraversalAttack do
Pathname.new(File.join("/var/app/uploads", OFFENDER_PATH)).cleanpath
end
end

test "detects the Pathname.cleanpath bypass used to reach sensitive files" do
raw_input = "../../../../etc/passwd"
set_context_from_request_to "/?filename=#{raw_input}"

assert_attack Aikido::Zen::Attacks::PathTraversalAttack do
Pathname.new(File.join("/var/app/uploads", raw_input)).cleanpath
end
end
end
end
Loading