1 |
8 |
ahitrov@rambler.ru |
package Utils::Spam::SecretForm; |
2 |
|
|
|
3 |
|
|
use strict; |
4 |
|
|
use Digest::MD5; |
5 |
|
|
use Contenido::Globals; |
6 |
|
|
use Scalar::Util qw(blessed); |
7 |
|
|
|
8 |
|
|
# Генерация строки для вставки в hidden-поле формы |
9 |
|
|
# для подтвеждения действий пользователя. |
10 |
|
|
# extra - дополнительный параметр для генерации строки |
11 |
|
|
# (к примеру, если человек залогинен, можно передавать его логин) |
12 |
|
|
# Не забыть передавать данный параметр при валидации. |
13 |
|
|
sub generate { |
14 |
|
|
my %opts = @_; |
15 |
|
|
|
16 |
|
|
my ($start_time, $secret_code) = &get_secret_code(allow_generate_code => 1, memd => $opts{'memd'}); |
17 |
|
|
return unless $start_time && $secret_code; |
18 |
|
|
|
19 |
|
|
my $random = &get_random_string(5); |
20 |
|
|
# start secret code time | generate time | hash |
21 |
|
|
return $start_time.'|'.time().'|'.$random.'|'.Digest::MD5::md5_hex($start_time.$secret_code.$random.$opts{'extra'}); |
22 |
|
|
} |
23 |
|
|
|
24 |
|
|
# Процедура проверки hidden-поля формы |
25 |
|
|
# secret - код, для проверки |
26 |
|
|
# ttl - время жизни выданного кода |
27 |
|
|
# check_count - true означает разрешение на подсчет количества использований |
28 |
|
|
sub validate { |
29 |
|
|
my %opts = @_; |
30 |
|
|
|
31 |
|
|
my $user_secret = $opts{'secret'}; |
32 |
|
|
my $ttl = $opts{'ttl'} || 3600; |
33 |
|
|
my $extra = $opts{'extra'}; |
34 |
|
|
my $check_count = $opts{'check_count'}; |
35 |
|
|
my $result = { is_valid => 1, is_expired => 0, count => 1 }; |
36 |
|
|
my $memd = blessed($opts{'memd'}) ? $opts{'memd'} : $keeper->MEMD(); |
37 |
|
|
|
38 |
|
|
return $result unless blessed($memd); |
39 |
|
|
|
40 |
|
|
my ($user_start_time, $user_generate_time, $user_random, $user_hash) = split(/\|/, $user_secret); |
41 |
|
|
|
42 |
|
|
# данные о секретной строке |
43 |
|
|
my ($start_time, $secret_code) = &get_secret_code(time => $user_start_time, memd => $memd); |
44 |
|
|
|
45 |
|
|
# Если кэш не работает принимаем любые параметры |
46 |
|
|
return $result if !defined($start_time) && !defined($secret_code); |
47 |
|
|
|
48 |
|
|
if (Digest::MD5::md5_hex($start_time.$secret_code.$user_random.$extra) ne $user_hash) { |
49 |
|
|
$result->{'is_valid'} = 0; |
50 |
|
|
} |
51 |
|
|
|
52 |
|
|
if ($result->{'is_valid'} && (($user_generate_time-$start_time > 3600) |
53 |
|
|
|| (time()-$user_generate_time > $ttl)) |
54 |
|
|
) { |
55 |
|
|
$result->{'is_expired'} = 1; |
56 |
|
|
} |
57 |
|
|
|
58 |
|
|
# Если необходима проверка на повторное использование |
59 |
|
|
# Данные о количестве использования необходимо сохранить в кэше |
60 |
|
|
if ($result->{'is_valid'} && !$result->{'is_expired'} && $check_count) { |
61 |
|
|
|
62 |
|
|
$result->{'count'} = $memd->incr('usersecret|'.$user_secret, 1) if $memd; |
63 |
|
|
|
64 |
|
|
unless ($result->{'count'}) { |
65 |
|
|
$memd->add('usersecret|'.$user_secret, 1, $ttl) if $memd; |
66 |
|
|
$result->{'count'} = 1; |
67 |
|
|
} |
68 |
|
|
} |
69 |
|
|
|
70 |
|
|
return $result; |
71 |
|
|
} |
72 |
|
|
|
73 |
|
|
# Процедура, которая возвращает true или false в зависимости от валидности, просроченности |
74 |
|
|
# и количества использований |
75 |
|
|
sub is_valid_secret { |
76 |
|
|
my %opts = @_; |
77 |
|
|
|
78 |
|
|
my $validate = &validate(%opts); |
79 |
|
|
|
80 |
|
|
if ($validate->{'is_valid'} && !$validate->{'is_expired'} |
81 |
|
|
&& (!$opts{'check_count'} || ($opts{'check_count'} && $validate->{'count'} == 1))) |
82 |
|
|
{ |
83 |
|
|
return 1; |
84 |
|
|
} |
85 |
|
|
|
86 |
|
|
return undef; |
87 |
|
|
} |
88 |
|
|
|
89 |
|
|
# Процедура получения и генерации при необходимости |
90 |
|
|
# секретного кода. Сохраняет свою атуальность в течение суток. |
91 |
|
|
# time - для указанного времени вернет актуальный на тот момент код |
92 |
|
|
# allow_generate_code - разрешает генерировать код, если он не найден |
93 |
|
|
sub get_secret_code { |
94 |
|
|
my %opts = @_; |
95 |
|
|
|
96 |
|
|
# Наличие объекта memcached является обязательным |
97 |
|
|
# условием работоспособности |
98 |
|
|
my $memd = blessed($opts{'memd'}) ? $opts{'memd'} : $keeper->MEMD(); |
99 |
|
|
return unless blessed($memd); |
100 |
|
|
|
101 |
|
|
my $time = abs(int($opts{'time'})); |
102 |
|
|
my $now_time = time(); |
103 |
|
|
|
104 |
|
|
# Время с округлением до начала часа |
105 |
|
|
unless ($time) { |
106 |
|
|
$time = $now_time; |
107 |
|
|
$time = $time - ($time % 3600); |
108 |
|
|
} |
109 |
|
|
|
110 |
|
|
my $cache_key = 'secret_code|'.$time; |
111 |
|
|
|
112 |
|
|
my $secret_code = $memd->get($cache_key); |
113 |
|
|
return ($time, $secret_code) if $secret_code; |
114 |
|
|
|
115 |
|
|
if ($opts{'allow_generate_code'}) { |
116 |
|
|
$secret_code = &get_random_string(10); |
117 |
|
|
$memd->set($cache_key, $secret_code); |
118 |
|
|
} |
119 |
|
|
|
120 |
|
|
return ($time, $secret_code); |
121 |
|
|
} |
122 |
|
|
|
123 |
|
|
|
124 |
|
|
# Генерация случайной строки |
125 |
|
|
# length - длина строки |
126 |
|
|
sub get_random_string { |
127 |
|
|
my $length = shift; |
128 |
|
|
$length = 10 unless $length && $length =~ /^\d+$/; |
129 |
|
|
|
130 |
|
|
my $random_chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; |
131 |
|
|
my $random_chars_length = length($random_chars); |
132 |
|
|
|
133 |
|
|
my $string = ''; |
134 |
|
|
for (1..$length) { |
135 |
|
|
$string .= substr($random_chars, int(rand($random_chars_length)), 1); |
136 |
|
|
} |
137 |
|
|
|
138 |
|
|
return $string; |
139 |
|
|
} |
140 |
|
|
|
141 |
|
|
|
142 |
|
|
1; |
143 |
|
|
__END__ |
144 |
|
|
|
145 |
|
|
=head1 NAME |
146 |
|
|
|
147 |
|
|
Utils::Spam::SecretForm - утилиты генерации и проверки секретной строки для web-форм |
148 |
|
|
|
149 |
|
|
=head1 SYNOPSIS |
150 |
|
|
|
151 |
|
|
Генерация: |
152 |
|
|
<input type="hidden" name="secret" value="<% Utils::Spam::SecretForm::generate() %>"> |
153 |
|
|
|
154 |
|
|
Проверка: |
155 |
|
|
my $validate = Utils::Spam::SecretForm::validate( secret => $ARGS{'secret'}, check_count => 0|1 ); |
156 |
|
|
if ($validate->{'is_valid'} && !$validate->{'is_expired'}) { |
157 |
|
|
allowed method |
158 |
|
|
} |
159 |
|
|
|
160 |
|
|
|
161 |
|
|
=head1 DESCRIPTION |
162 |
|
|
|
163 |
|
|
С помощью javascript существует способ подделки http-запроса get и post методов. |
164 |
|
|
Некая страница a.html содержит: |
165 |
|
|
<form method=post action=http://www.rambler.ru/post.html name="b"> |
166 |
|
|
<input type="text" name="text"> |
167 |
|
|
<input name="submit" type="submit" value="submit"> |
168 |
|
|
</form> |
169 |
|
|
<script> |
170 |
|
|
document.b.submit.click(); |
171 |
|
|
</script> |
172 |
|
|
|
173 |
|
|
Таким образом, человек, зашедщий на страницу a.html, отправит данную форму. |
174 |
|
|
Для борьбы с данным видом атак следует использовать данный модуль. |
175 |
|
|
|
176 |
|
|
Принцип работы: раз в час генерируется случайная секретная строка, которая хранится в кэше по ключу secret_code|время_генерации. |
177 |
|
|
Если кэш не работает, валидация проходит успешно для любого рода запросов. |
178 |
|
|
Пользователь при генерации формы получает некий код в hidden поле, который состоит из трех частей: |
179 |
|
|
1) время генерации секретной строки |
180 |
|
|
2) время выдачи строки пользователю |
181 |
|
|
3) рандомная строка |
182 |
|
|
4) md5_hex( время выдачи строки пользователю . рандомная строка . секретная строка) |
183 |
|
|
При верификации полученного hidden-параметра происходит получение секретной строки из кэша secret_code|время_генерации, сравнение md5_hex(времени . полученной строки) и проверка разницы времени установки поля и текущего времени. |
184 |
|
|
Таким образом получаем защиту от несанкционированных запросов. Для защиты от повторного использования кода необходимо использовать параметр check_count, в зависимости от которого в результате верификации вернется количество использования кода (после первого использование count = 1). |
185 |
|
|
Для повышения защиты существует возможность добавления extra параметра для генерации hidden-кода, например для залогиненого пользователя, это может быть логин или сессия. Но необходимо не забывать передавать этот параметр во время валидации. |
186 |
|
|
|