严肃的表单安全

Avatar of Chris Coyier
Chris Coyier 发布

DigitalOcean 为您的旅程每个阶段提供云产品。 立即开始使用 价值 200 美元的免费积分!

关于 网站变更请求表单 的话题在我们这里已经讨论了一段时间,我将继续讨论一段时间。 我们不会重新讨论使表单正常工作的全部 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,它非常易于使用,有助于人们,并且非常易于访问。

安全专家?

当我们在讨论安全问题时,人们往往对事情的处理方式有很多意见。 如果您有想法,请在下方尽可能建设性地分享,我将认真消化并看看如何在前进的道路上进行改进。

查看演示 下载文件