07.04.2010Немного о $SAFE
Введение
Совершенно не по работе заинтересовался переменной $SAFE и её ролью в жизни современного разработчика. Оказалось, что всё нужно проверять самому.
Нежная безопасность
Для тестирования возможностей на разных уровнях безопасности собрал небольшую программку. Она просит ввести имя файла, делая строковую переменную небезопасной, и пытается что-то с этим всем сделать.
print "child: "
child = gets.chomp
puts "child tainted: #{child.tainted?}"
(0..4).to_a.each do |i|
puts "SAFE: #{i}"
$a = "safe"
th = Thread.new do
$SAFE = i
child_copy = child.dup
Thread.current[:out] = ""
begin
load child_copy
Thread.current[:out] += "1. Child loaded\n"
rescue SecurityError => e
Thread.current[:out] += "1. Security error: #{e.to_s}\n"
begin
child_copy.untaint
load child_copy
Thread.current[:out] += "2. Child untainted and loaded\n"
rescue SecurityError => e
Thread.current[:out] += "2. Security error: #{e.to_s}\n"
begin
Thread.current[:out] += "3. Read from file '#{child_copy}': '#{File.read(child_copy)}'\n"
rescue SecurityError => e
Thread.current[:out] += "3. Security error: #{e.to_s}\n"
begin
Thread.current[:out] += "4. Read from untainted file: '#{File.read("child.rb")}'\n"
rescue SecurityError => e
Thread.current[:out] += "4. Security error: #{e.to_s}\n"
end
end
end
end
begin
$a = "modified"
Thread.current[:out] += "5. Global variable modified: $a = '#{$a}'\n"
rescue SecurityError => e
Thread.current[:out] += "5. Security error: #{e.to_s}\n"
end
begin
Dir.mkdir "test"
Thread.current[:out] += "6. Created directory 'test': #{File.exist?("test")}\n"
Dir.rmdir "test"
rescue SecurityError => e
Thread.current[:out] += "6. Security error: #{e.to_s}\n"
end
begin
Thread.current[:out] += "7. Dir glob: #{Dir.glob(File.join("..", "*")).inspect}\n"
rescue SecurityError => e
Thread.current[:out] += "7. Security error: #{e.to_s}\n"
end
begin
Thread.current[:out] += "8. System ls output: '#{`ls`.chomp}'"
rescue SecurityError => e
Thread.current[:out] += "8. Security error: #{e.to_s}\n"
end
end
th.join
puts "Global variable: $a = '#{$a}'"
puts th[:out] if th[:out]
end
Конструкция со Thread.current[:out] используется потому, что для $SAFE >= 4 нельзя ничего писать ни в какие устройства вывода.
Вроде бы всё логично. Первый уровень годится для умеренного карантина внешних данных. При желании их можно и расколдовать. Второй уровень запрещает изменения в файловой системе. Третий уровень похож на осаду с постоянным подозрением на шпионаж. Все созданные объекты считаются небезопасными. А четвёртый уровень — это самое близкое к песочнице (sandbox) в руби, что что есть.
Кстати, когда ещё github работал как репозиторий библиотек, спецификация gemspec выполнялась там под $SAFE = 3. Для разработчиков это выливалось в то, что нужно было перечислять все файлы своей библиотеки вручную вместо использования какого-нибудь листинга.
Суровый гайдлайн
Конечно же, только использование $SAFE не убережёт от действительно настойчивой атаки или блокирующего кода. Например:
Thread.new do
$SAFE = 2
class String
def ==(other_string)
true
end
end
end.join
puts "string modified: #{'a' == 'b'}"
И это на втором уровне! А на третьем открыть класс тоже можно, но вызов перегруженного оператора будет вызывать SecurityError.
На сегодняшний момент эту концепцию безопасности можно считать сырой. Актуальное поведение руби 1.8 слегка отклоняется от описаний, что я нашёл. Поведение в 1.9 изменилось, но подробно нигде не описано (я не нашёл).
Это не значит, что этой переменной нет применения в жизни прогрессивного человечества. Адекватное текущему состоянию применение — это гайдлайн при разработке. Руководство для программистов, которое само следит за своим исполнением. Жестковато, но зато действенно. :)
Материалы для самостоятельного изучения
- Код примеров в статье на github
- Старая, но самая подробная документация по $SAFE
- Просто дополнительно: шпаргалка по руби