2013-04-22

Веб-боты и защита от них

На самом деле, заметку начну с рассказа о простой защите от ботов, отправляющих формы и завершу информацией о собственной реализации такого бота. Картиночные и прочие капчи, предлагающие пользователю доказать, что он человек, тут рассматриваться не будут.

Защищаемся от ботов без помощи пользователя

Не будем утруждать пользователя решением капчи. Попробуем защититься от ботов с помощью JavaScript. Это слабая защита, но все равно позволяется справляться с большинством ботов.
Для примера, расскажу как мне приходилось защищаться от автоматических ботов на сайте на Битрикс.
В Битрикс уже имеется защита от автоматической отправки - в форму добавляется поле с именем sessid и при его отсутствии или не правильном значении форма не будет принята. Но боты это обходят (как именно это реализуется - будет описано ниже). Попробуем усложнить им задачу. Секретное поле выводится с помощью функции:
<form ...><?=bitrix_sessid_post()?>...</form>
Попробуем вынести это поле вне формы и повесить обработчик на отправку формы, в котором мы будем динамически яваскриптом возвращать скрытое поле в форму. Да, это помогает отфутболивать большинство ботов.
Если бот обучен таким хитростям Битрикс и сам находит в коде sessid и подставляет его в форму, то тут можно попробовать создать обязательное поле, пустое по умолчанию и перед отправкой формы динамически его заполнять яваскриптом.
В любой другой CMS можно использовать подобный подход с динамическим обязательным полем.

Пишем бота

Опять же рассмотрю пример автоматической отправки на примере сайта Битрикс.
sessid каким-то образом связан с сессией, поэтому наш бот должен работать с cookies.
Следующий алгоритм можно реализовать можно на разных ЯП:
1) Обратиться к странице и прочитать ее заголовки и контент.
2) Из заголовков взять cookie.
3) Из контента взять sessid.
4) Задать заголовки (cookie, browser-agent и т.п.) запроса.
5) Отправить post.
Пример релизуем на ruby.
# будем пользоваться гемом socksify
require 'socksify/http'
Сделаем бота более похожим на человека. Для этого найдем список из различных вариантов для подстановки в user-agent:
useragents = File.readlines('useragents.txt')
Далее, возьмем Tor для работы через разные IP.
Теоретически, следующий код должен сменять IP по команде, но у меня менялся автоматически через какое-то время, а не по запросу на управляющий порт:

begin
# control port
t = TCPSocket.new('127.0.0.1', 9151)
rescue
puts "error: #{$!}"
else
# кажется, это не работает, ну да пусть будет
puts 'signal newnym'
# t.print "signal newnym"
# t.puts "signal newnym"
t.write "signal newnym"
t.close
end
Запрос-ответ-запрос:
Net::HTTP.SOCKSProxy('127.0.0.1', 9050).start(conf[:domain]) do |http|
useragent = useragents.sample
# get
headers_get = {
'User-Agent' => useragent,
'Referer' => 'http://'+conf[:domain]+'/'
}
res = http.get(conf[:path], headers_get)
all_cookies = res.get_fields('set-cookie')
cookies_array = Array.new
all_cookies.each { | cookie |
cookies_array.push(cookie.split('; ')[0])
}
cookies = cookies_array.join('; ')
r = %r{'bitrix_sessid':'(\w+)'}.match res.body
sessid = r && r[1] ? r[1] : ''
# post
data = "SOME_FIELD_1=Y&OTHER_FIELD_2=123&sessid=#{sessid.to_s}"
headers_post = {
'User-Agent' => useragent,
'Referer' => 'http://'+conf[:domain]+'/',
'Cookie' => cookies
}
res = http.post(conf[:path], data, headers_post)
end
По желанию, добавим наш код в блок try/catch для большей надежности:
begin
... запросы ...
rescue =>err
em = "#{err.message}\n#{err.backtrace.map do |s| "\t" + s end.join("\n")}"
puts em
$logger.error em
rescue Timeout::Error => err
em = "#{err.message}\n#{err.backtrace.map do |s| "\t" + s end.join("\n")}"
puts em
$logger.error em
end
Еще можно добавить в запрос недостащие заголовки, чтобы бот был более похожим на браузер.
Вот так просто отказывается эмулировать браузер пользователя с поддержкой cookie и, следовательно, сессий.