关于 网站变更请求表单 的话题在我们这里已经讨论了一段时间,我将继续讨论一段时间。 我们不会重新讨论使表单正常工作的全部 HTML 和 JavaScript,因此,如果您需要赶上来,请查看第一篇文章。
目前,我们有一个外观不错的表单,它拥有不错的用户体验。 但是,我觉得它缺少两项主要内容。 A) 通知电子邮件本身非常平淡,是基本文本电子邮件;B) 表单本身几乎没有任何安全性。
感谢 Daniel Friedrich,我已经在表单中实现了更严肃的安全措施,这将成为本文的重点。 两个主要目标是
- 表单是由人类提交的
- 该人类没有做任何恶意的事情

令牌匹配
我们要做的第一件事是生成一个“令牌”,本质上是一个秘密代码。 该令牌将成为我们“会话”的一部分,这意味着它存储在服务器端。 该令牌**也**将在表单首次在浏览器中生成时作为隐藏输入应用于表单本身。 这意味着该令牌**同时**存在于客户端和服务器端,并且我们可以在提交表单时匹配它们并确保它们相同。 这可以确保任何提交的表单**都是我们的表单**,而不是来自不同服务器的第三方脚本对我们进行攻击。
首先,我们需要启动一个会话,然后创建一个函数来构建令牌,将其应用于会话,并将其返回以供我们使用。
session_start();
function generateFormToken($form) {
// generate a token from an unique value
$token = md5(uniqid(microtime(), true));
// Write the generated token to the session variable to check it against the hidden field when the form is sent
$_SESSION[$form.'_token'] = $token;
return $token;
}
此处的唯一值来自 microtime 函数的 md5 哈希,但 Daniel 说还有很多其他加密方法(如盐值……)。 现在,我们已经有了此函数,在我们在标记中构建表单之前,我们可以调用它来创建该值。
<?php
// generate a new token for the $_SESSION superglobal and put them in a hidden field
$newToken = generateFormToken('form1');
?>
然后将令牌作为隐藏输入放入表单本身
<input name="token" type="hidden" value="<?php echo $newToken; ?>">
现在,我们已准备好提交表单时相互检查令牌值。 我们将创建一个函数来执行此操作。
function verifyFormToken($form) {
// check if a session is started and a token is transmitted, if not return an error
if (!isset($_SESSION[$form.'_token'])) {
return false;
}
// check if the form is sent with token in it
if (!isset($_POST['token'])) {
return false;
}
// compare the tokens against each other if they are still the same
if ($_SESSION[$form.'_token'] !== $_POST['token']) {
return false;
}
return true;
}
此函数检查令牌是否存在于两个必需位置,以及它们是否匹配。 如果这三件事都为真,则该函数返回 true,否则,它返回 false。 现在,我们在继续之前检查该值。 基本结构是
if (verifyFormToken('form1')) {
// ... more security testing
// if pass, send email
} else {
echo "Hack-Attempt detected. Got ya!.";
writeLog('Formtoken');
}
黑客记录
请注意,在上面的结构中,我们使用了名为 writeLog() 的函数,该函数接受一个字符串。 在我们检测到违规行为并需要停止的情况下,我们会调用 writeLog() 函数来记录错误以供我们自己参考,然后执行 die()
此函数尝试写入服务器上的文本文件(该文件需要适当的文件权限,用户可写),如果失败,它会将该文件通过电子邮件发送给您。
function writeLog($where) {
$ip = $_SERVER["REMOTE_ADDR"]; // Get the IP from superglobal
$host = gethostbyaddr($ip); // Try to locate the host of the attack
$date = date("d M Y");
// create a logging message with php heredoc syntax
$logging = <<<LOG
\n
<< Start of Message >>
There was a hacking attempt on your form. \n
Date of Attack: {$date}
IP-Adress: {$ip} \n
Host of Attacker: {$host}
Point of Attack: {$where}
<< End of Message >>
LOG;
// open log file
if($handle = fopen('hacklog.log', 'a')) {
fputs($handle, $logging); // write the Data to file
fclose($handle); // close the file
} else { // if first method is not working, for example because of wrong file permissions, email the data
$to = '[email protected]';
$subject = 'HACK ATTEMPT';
$header = 'From: [email protected]';
if (mail($to, $subject, $logging, $header)) {
echo "Sent notice to admin.";
}
}
}
未发布我们没有要求的任何内容
如果向我们发布了任何名称与我们自己表单中的输入不匹配的值,那么**肯定**有问题。 我们将构建一个可接受的发布名称“白名单”,然后检查每个名称。
// Building a whitelist array with keys which will send through the form, no others would be accepted later on
$whitelist = array('token','req-name','req-email','typeOfChange','urgency','URL-main','addURLS', 'curText', 'newText', 'save-stuff');
// Building an array with the $_POST-superglobal
foreach ($_POST as $key=>$item) {
// Check if the value $key (fieldname from $_POST) can be found in the whitelisting array, if not, die with a short message to the hacker
if (!in_array($key, $whitelist)) {
writeLog('Unknown form fields');
ie("Hack-Attempt detected. Please use only the fields in the form");
}
}
有效 URL
此表单的客户端验证会监视这种情况,因此应该在前端捕获,但当然我们也应该在后端进行检查。
// Lets check the URL whether it's a real URL or not. if not, stop the script
if (!filter_var($_POST['URL-main'],FILTER_VALIDATE_URL)) {
writeLog('URL Validation');
die('Please insert a valid URL');
}
清理值
至此,我们已经完成了所有安全检查,并将继续创建和发送电子邮件。 将所有输入完全按输入的方式发送是一个潜在的安全风险。 例如,可以在文本字段中输入 JavaScript,然后通过电子邮件发送,并且在打开电子邮件时可能会运行。
对于“名称”之类的字段,没有理由使用任何特殊标签,我们将使用 strip_tags() 将它们完全删除。 对于可能需要使用一些标签的文本区域,我们将使用 htmlentities() 函数将其安全地转换。
示例
$message .= "Name: " . strip_tags($_POST['req-name']) . "\n";
$message .= "NEW Content: " . htmlentities($_POST['newText']) . "\n";
更好地清理
Krinkle 写道
最初,您对普通字段使用 strip_tags(),对内容使用
htmlentities()
。 这很好,只是最好也声明 ENT_NOQUOTES 和“UTF-8”,否则,像重音符号 e(“é”)之类的字符可能会变成 Å@ 这样的垃圾。 而且,由于大多数服务器会对通过 $_POST[] 带来的输入添加斜杠,因此运行 stripslashes 并不是一件坏事,以防万一,否则,一旦输入到邮箱中,在表单中输入的每个单引号或双引号前面都会有一个斜杠。
function stripcleantohtml($s){
// Restores the added slashes (ie.: " I\'m John " for security in output, and escapes them in htmlentities(ie.: " etc.)
// Also strips any <html> tags it may encounter
// Use: Anything that shouldn't contain html (pretty much everything that is not a textarea)
return htmlentities(trim(strip_tags(stripslashes($s)), ENT_NOQUOTES, "UTF-8"));
}
function cleantohtml($s){
// Restores the added slashes (ie.: " I\'m John " for security in output, and escapes them in htmlentities(ie.: " etc.)
// It preserves any <html> tags in that they are encoded aswell (like <html>)
// As an extra security, if people would try to inject tags that would become tags after stripping away bad characters,
// we do still strip tags but only after htmlentities, so any genuine code examples will stay
// Use: For input fields that may contain html, like a textarea
return strip_tags(htmlentities(trim(stripslashes($s)), ENT_NOQUOTES, "UTF-8"));
}
在此示例中的用法
$message .= "Name: " . stripcleantohtml($_POST['req-name']) . "\n";
$message .= "NEW Content: " . cleantohtml($_POST['newText']) . "\n";
为什么不使用 CAPTCHA?
CAPTCHA 在防止垃圾邮件进入表单方面做得相当不错,但我们更担心的是黑客行为,而不是垃圾邮件。 此外,由于此表单是供我们可能**喜欢**且试图**帮助**的人使用的,因此我们不会让他们经历 CAPTCHA 烦人的跳跃。 但是,如果您有兴趣,一个超级简单的自制验证码是询问类似“十减五等于多少?”这样的问题,并在文本输入中检查“5”以及“five”的大写字母组合,如果匹配,则继续,否则,不继续。 此版本的表单已经具有 writeLog() 函数,可以随时使用,逻辑可以很好地放在第 75 行和第 76 行周围。
如果您想要更强大的 CAPTCHA,请查看 reCAPTCHA,它非常易于使用,有助于人们,并且非常易于访问。
安全专家?
当我们在讨论安全问题时,人们往往对事情的处理方式有很多意见。 如果您有想法,请在下方尽可能建设性地分享,我将认真消化并看看如何在前进的道路上进行改进。
不错的文章,对于创建用户输入的每个网页设计师来说都非常重要。
有一点:Md5 加密。 这通常被称为“不太安全,最好使用 SHA-256 位加密”。 但是,如何执行此操作? PHP 中应该有一些 sha256(string) 函数,但我无法使其正常工作。
第一个谷歌搜索结果:http://www.nanolink.ca/pub/sha256/
是的,我注意到了。 但是我想在我的 PHP 文件中使用它,它不起作用 :-/ 比如 echo sha256('hello'); 在这里没有输出任何内容。 我已经谷歌搜索了很多,不用担心。 我实际上是在寻找诸如“哦,他只是忘记了……”之类的回复,因为我认为这是一个我没想到的简单事情。 但无论如何,谢谢! :-)
我认为 这是您要找的。
(反CSRF)令牌存储在会话中。而会话仅存储半小时。你不可能在这半小时内得到MD5碰撞。
而且MD5不是加密算法,它是哈希算法,因此它不能被解密。:)
令牌用于保护用户免受CSRF攻击。它只是一个随机字符串,作为一种ID与表单数据一起发送,其他人没有。如果没有正确的令牌/ID,表单将不会被处理。
但它可以被查询。想想字典攻击。
但是,仅仅向表单发送POST请求是行不通的,因为你首先需要一个令牌。要获得该令牌,你必须访问该网站(GET请求)。
因此,以字典攻击形式发送POST请求是行不通的。
哦,是的。有一个网站,它的数据库包含值和它们的哈希值,以及大约12亿个条目。这真是太疯狂了。
很棒的文章——每个网站管理员都应该在考虑验证码解决方案之前,仔细阅读这些步骤。
除了提供更好的用户体验之外,通常也存在切实的商业利益——我们最近从我们的注册表单中删除了ReCAPTCHA,并使用了一些这些技术。这使得我们的转化率提高了两位数。
这是一个很棒的想法。我喜欢它。哈希算法很棒。
我有一个与主题无关的问题。为什么我最近访问的许多网站设计者的网站没有在其锚标签中使用 target="_blank"?它可以将外部网站与自己的网站分开,并减少一些普通“冲浪者”的困惑。
干得好,我真的很喜欢阅读这篇文章。大多数网站都没有任何安全措施,但本文将所有内容都放在了正确的位置。另外,不要忘记HTTPS和加盐等内容。
继续努力!
你使用 hash() 函数。
参见此处
示例
hash("sha256", "My string to be hashed");
我们去掉了CAPTCHA验证,使用Wufoo来提供表单,发现提交的表单数量大幅增加(翻了一番)。这很棒,因为我的老板一直在质疑每月费用,并想知道其他选择。
好文章!我认为令牌部分很棒。它不仅可以确保表单是从生成该表单的页面提交的,而且还可以与AJAX请求一起使用,以确保请求是由目标页面发出的。
克里斯,写得很好。一直在寻找验证码的替代方案。它们是对转换过程的攻击,也是对可用性的侮辱。
我能够将SQL注入到其中一个文本框中。
表单发送电子邮件,你注入了SQL?干得好,伙计!
你是否会发布你输入的内容以及输入到哪个字段?我想在我的表单上测试一下,以进行可靠的SQL注入尝试。
在一个不接触数据库的PHP文件中,不存在SQL注入这种东西。
你在这里做的是一个很好的防御措施,但请注意,有一些偷偷摸摸的方法可以在没有标签(或者我们通常认为的标签)的情况下注入JavaScript。
在某些时候,总是可能被黑客攻击。关键是要让它变得如此困难,以至于没有人愿意费心去尝试。
在成功提交后清除令牌,以防止人们在F5上“弹跳”并重复提交相同的表单。
我在一个网站上使用 reCAPTCHA,取得了相当不错的成功。如果我有时间的话,我可能会尝试一下这篇文章……
“这样做可以确保任何表单提交都是我们的表单,而不是来自不同服务器的第三方脚本对我们进行疯狂的攻击。”
为什么不使用PHP的$_SERVER['HTTP_REFERER']来查看请求是否来自你的表单页面?
因为它可以被客户端伪造。
因为referer字段很容易被篡改。客户端可以发送用户想要的任何值。例如,我的Opera根本不发送任何referer信息。
对HTTP_REFERRER进行额外的验证并不会造成任何伤害,所以如果有人正在考虑这样做,可以尝试一下。毕竟,并非所有的垃圾邮件发送者都很聪明*咳嗽*
你这里有一篇有趣的文章。我很快就会查看它。
确实,这是一篇有趣的文章。
克里斯,很棒的主题!鉴于wufoo现在出现故障,现在是开始自己编写表单的时候了。
这将非常有用!
永远不要,永远不要,永远不要,永远不要,永远不要,永远不要,永远不要,永远不要,永远不要使用MD5作为你的首选算法!永远不要!……除了没有使用安全套的情况下。
SHA1比MD5安全得多,但两者都可以被暴力破解,MD5比SHA1更容易被破解。
不过,对于所有哈希算法,你都应该使用一个SALT(我提到的安全套)。这可以使MD5和SHA1都更加安全。MD5大约需要200,000次尝试,SHA1大约需要一百万次尝试,而SHA1 + 前置SALT大约需要800万亿次尝试才能正确猜测。
因此,如果你要进行任何玩弄,请戴上SALT安全套。
这是否是一个更好的解决方案?
对此有何想法?
$salt = time();
$string = uniqid(microtime(), true);
switch( substr( $salt, -1, 1) ) {
case 1
$token = sha1( $salt . $string ) . substr( $salt , 1, 3);
break;
case 2
$token = sha1( $string . $salt ) . substr( $salt , 3, 2);
break;
case 3
$token = substr( $salt , 5, 2) . sha1( $salt . $string ) ;
break;
}
更好还是没有?
好多了,但我在sha1 PHP手册中也找到了这个(不是我的功劳,我并没有想到这个),它似乎也能很好地创建更难的伪盐。
<?php
function createHash($inText, $saltHash=NULL, $mode='sha1'){
// 对文本进行哈希运算 //
$textHash = hash($mode, $inText);
// 设置盐将在哈希中出现的位置 //
$saltStart = strlen($inText);
// 如果没有提供盐,则创建随机盐 //
if($saltHash == NULL) {
$saltHash = hash($mode, uniqid(rand(), true));
}
// 将盐添加到文本哈希中,位于密码长度位置,并对其进行哈希运算 //
if($saltStart > 0 && $saltStart < strlen($saltHash)) {
$textHashStart = substr($textHash,0,$saltStart);
$textHashEnd = substr($textHash,$saltStart,strlen($saltHash));
$outHash = hash($mode, $textHashEnd.$saltHash.$textHashStart);
} elseif($saltStart > (strlen($saltHash)-1)) {
$outHash = hash($mode, $textHash.$saltHash);
} else {
$outHash = hash($mode, $saltHash.$textHash);
}
// 将盐放在哈希的前面 //
$output = $saltHash.$outHash;
return $output;
}
?>
由于rand()使用时间来创建它的伪随机数,我认为这也是创建盐的一种好方法。
为了进一步防止黑客攻击,请尝试使用表单超时。
我在本文中对此进行了介绍:serversidemagazine.com/php/php-security-measures-against-csrf-attacks
此外,在该文章的评论中,一些读者讨论了AJAX CSRF攻击的想法,这是一种比较新的黑客攻击方法。
这是一篇有趣的文章,主题选择得很好。令牌和白名单都是很有趣的想法。我下次构建表单时一定会考虑使用这些技术。
感谢 Chris!
天才!
嗨,我想指出,在 ValidURL 检查器中,如果我输入
http://www.somesite.com
它返回了一个无效的 URL,但如果我添加 http://,它返回 true。
我可以看到该例程中可能存在缺陷,即客户可能不会输入 http://,因为他们会以简单的格式“www.sitename.com”来思考网站链接。
在我编写的应用程序中,我会检查字符串是否包含 http://——如果有,则保留字符串原样。如果未找到 http://,我会使用字符串函数 str_replace 添加它。
我可能错了,但我只是一名年轻的 Web 开发人员(20 岁),并且只做了 1 年,但它似乎合乎逻辑。
在 Firefox 中看起来很棒。在老式的 IE 7 和 8 中,下拉菜单被遮挡了。
想知道 z-index 是否能解决问题?
这些评论大多是胡说八道。此方法对于 CSRF 来说很好,但无法阻止任何人远程运行脚本,从表单中提取键-值并远程提交。
这种完全相同的方法已在开源系统中使用多年。这没什么新意。
我同意 Danny——在这个 CRSF 示例中,使用 MD5 不是问题。
例如,Django 的 CRSF 中间件使用 MD5
http://code.djangoproject.com/browser/django/trunk/django/contrib/csrf/middleware.py#L27
此实现中没有安全漏洞。
一般来说,这些评论证实了显而易见的事实——不要试图自己实现 CSRF——你会做错。相反,请使用知名/支持的库。Django 使用 CRSF 中间件——其他语言和框架无疑有自己的实现。
非常有见地的文章。作为旁注,在您的表单中
其他 URL/区域
应该是
其他 URL/区域
没有撇号。:)
嗨 Chris,
人们显然在这里讨论了很多开发——我在想表单的可用性。有一些东西我遗漏了,或者我认为是缺陷
1) 对于客户来说,发送电子邮件比填写/提交表单更快。
2) 一些客户会在文档 (PDF、Word) 中准备更改。他们为什么要使用表单?
2) 表单无法处理复杂的更改(跨多个页面进行多个更改、多个相关更改、设计更改等)
3) 表单无法上传和发送媒体文件(文档、图像)
仅仅是举几个例子。我认为大多数客户不会使用表单进行更改。
欢呼
嗨 Chris,
关于表单安全的不错的教程,这可以作为验证码的替代方案,它也能阻止机器人吗?
Ciao
嗨,
还有一个问题,此方法可以保护表单免受 xrumer 的攻击吗?
谢谢
为什么此表单未发送?我已经更改了相应的电子邮件?
CSRF 防护并非万无一失——只需向表单页面发送一个 GET 请求,解析令牌并将其与会话信息一起转移到 POST 请求中即可绕过。鉴于存在通过填写表单并提交表单(而不仅仅是提交表单)来工作的垃圾邮件机器人,因此这种方法需要与另一种方法结合使用。
我下载了演示文件。感谢分享。但 PHP 代码的最后部分似乎不完整,在 mail() 函数之后,在 HTML 开始之前。有一个“if”子句没有主体。我不知道这个博客如何处理代码,但我会尝试
} else {
if (!isset($_SESSION[$form.’_token’])) {
//这里应该放点什么?
} else {
echo “检测到黑客攻击。抓到你了!。”;
writeLog(‘Formtoken’);
}
哇
太奇怪了。我写了一篇帖子,结果出现在最后一篇帖子上面的第 4 篇帖子中。从未见过这种情况。无论如何……上面的第 4 篇帖子中有一个关于演示文件中空“if”条件的问题。谁能帮我弄清楚为什么它在那里,以及它可能缺少什么?谢谢!
啊,不用担心。怪事。我 7 月 2 日写的帖子出现在 5 月 19 日和 20 日写的帖子之前。
只是一个想法,但您可能最好将输入的名称设置为令牌。这样,如果有人尝试使用 GET 请求获取令牌,输入的名称将不同。无论如何可能更好一点。
另外,md5 sha1,无论是什么。我不认为这有任何关系。这里没有加密信息,所以不用担心。您只是在验证它是否相同。真正重要的是令牌必须相当唯一,并且可以验证它在每个请求中都相同。
黑客可以使用正则表达式轻松获取输入中的令牌值。
例如:使用 AJAX 或 curl 打开我们的登录页面,并使用正则表达式从隐藏输入中获取令牌值。
如何防止这种情况?
嗨,对于所有想知道如何阻止黑客获取令牌的人,可以使用
谢谢,这将 cookie 保存为 Whirpool,并以安全的方式执行,并且仅限 http(无 AJAX 请求)
我不是专家,但如果它有用的话,我不知道。