Skip to content

Commit f8deea5

Browse files
committed
Use proper randomness when generating CAPTCHAs
1 parent 0a6b066 commit f8deea5

File tree

1 file changed

+87
-2
lines changed

1 file changed

+87
-2
lines changed

system/helpers/captcha_helper.php

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,94 @@ function create_captcha($data = '', $img_path = '', $img_url = '', $font_path =
125125
if (empty($word))
126126
{
127127
$word = '';
128-
for ($i = 0, $mt_rand_max = strlen($pool) - 1; $i < $word_length; $i++)
128+
$pool_length = strlen($pool);
129+
$rand_max = $pool_length - 1;
130+
131+
// PHP7 or a suitable polyfill
132+
if (function_exists('random_int'))
133+
{
134+
try
135+
{
136+
for ($i = 0; $i < $word_length; $i++)
137+
{
138+
$word .= $pool[random_int(0, $rand_max)];
139+
}
140+
}
141+
catch (Exception $e)
142+
{
143+
// This means fallback to the next possible
144+
// alternative to random_int()
145+
$word = '';
146+
}
147+
}
148+
}
149+
150+
if (empty($word))
151+
{
152+
// Nobody will have a larger character pool than
153+
// 256 characters, but let's handle it just in case ...
154+
//
155+
// No, I do not care that the fallback to mt_rand() can
156+
// handle it; if you trigger this, you're very obviously
157+
// trying to break it. -- Narf
158+
if ($pool_length > 256)
159+
{
160+
return FALSE;
161+
}
162+
163+
// We'll try using the operating system's PRNG first,
164+
// which we can access through CI_Security::get_random_bytes()
165+
$security = get_instance()->security;
166+
167+
// To avoid numerous get_random_bytes() calls, we'll
168+
// just try fetching as much bytes as we need at once.
169+
if (($bytes = $security->get_random_bytes($pool_length)) !== FALSE)
170+
{
171+
$byte_index = $word_index = 0;
172+
while ($word_index < $word_length)
173+
{
174+
if (($rand_index = unpack('C', $bytes[$byte_index++])) > $rand_max)
175+
{
176+
// Was this the last byte we have?
177+
// If so, try to fetch more.
178+
if ($byte_index === $pool_length)
179+
{
180+
// No failures should be possible if
181+
// the first get_random_bytes() call
182+
// didn't return FALSE, but still ...
183+
for ($i = 0; $i < 5; $i++)
184+
{
185+
if (($bytes = $security->get_random_bytes($pool_length)) === FALSE)
186+
{
187+
continue;
188+
}
189+
190+
$byte_index = 0;
191+
break;
192+
}
193+
194+
if ($bytes === FALSE)
195+
{
196+
// Sadly, this means fallback to mt_rand()
197+
$word = '';
198+
break;
199+
}
200+
}
201+
202+
continue;
203+
}
204+
205+
$word .= $pool[$rand_index];
206+
$word_index++;
207+
}
208+
}
209+
}
210+
211+
if (empty($word))
212+
{
213+
for ($i = 0; $i < $word_length; $i++)
129214
{
130-
$word .= $pool[mt_rand(0, $mt_rand_max)];
215+
$word .= $pool[mt_rand(0, $rand_max)];
131216
}
132217
}
133218
elseif ( ! is_string($word))

0 commit comments

Comments
 (0)