Line # Revision Author
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