patternphpMinorCanonical
Preventing email injection - Part 2
Viewed 0 times
injectionpreventingpartemail
Problem
A day ago I have asked a question on here about Preventing email injection. I had some feedback and worked on it, and below is the latest update.
Could anyone please share their opinion? Is it secure enough? Is there a way to short-cut the code?
Could anyone please share their opinion? Is it secure enough? Is there a way to short-cut the code?
Invalid Email Address!";
break;
case !preg_match($spam_pattern, $name):
case !preg_match($spam_pattern, $number):
case !preg_match($spam_pattern, $date):
case !preg_match($spam_pattern, $select):
case !preg_match($spam_pattern, $radio):
case !preg_match($spam_pattern, $checkbox):
case !preg_match($spam_pattern, $message):
$response = "Invalid Request!";
break;
default:
$to = "";
$subject = "New Message From: $name";
$message = "Name: $name
number: $number
date: $date
select: $select
radio: $radio
checkbox: $checkbox
Email: $email
Message: $message";
$headers = 'MIME-Version: 1.0' . "\r\n";
$headers .= 'Content-type: text/html; charset=utf-8' . "\r\n";
$headers .= 'From: '.$email . "\r\n";
if(mail($to, $subject, $message, $headers)){
$response = "Success! Your submission has been sent.";
}else{
$response = "Error! There was a problem with sending.";
}
break;
}
}
}
else {
$response = "Error";
}
ob_flush();
}
?>Solution
- Due to the nested and long conditions the code becomes hard to follow
at some points. Early returns prevent nesting of conditions.
- Some code is duplicate (step 9)
- Possibly some values could be overwritten by url-forging (depending on the server-configuration). (step 5)
I'll do a step-by-step refactoring here with some explanations.
-
Move everything in a function. This eases early-return.
function sendMail() {
// your code here.
}-
While not necessary, it's I like to invert the conditions so the static expression stands on the left side. This avoids accidental assignment in conditions. Avoids tricky to detect bugs (though most IDEs and codesniffer emit a warning).
-
Using the
=== operator for comparison avoids bugs to unforeseen castingfunction sendMail() {
if ('POST' !== $_SEVER['REQUEST_METHOD']) {
return '';
}
if ($_SESSION['token'] !== $_POST['token']) {
return '0';
}
// remaining code
}-
What's the purpose of your output-cachting? I can't see any outputs being made? Trying to catch any errors? Hiding them is a bad idea, catch and handle them. If simply removed them.
- You are using
$_REQUESTinstead of$_POST. Sure you do check if the request type. But this doesn't prevent anyone from submitting both, GET and POST values. And here lies a dangerous problem: If I send you a link toform.php?message=somethingthis does get send to the server as well when you submit the form. Now GET and POST both having the keynamecollide. Depending on the variables_order the GET value I set overwrites your message. If you use$_POSTto access your variables instead this issue can be prevented.
-
I have extracted a simple accessor function to check for existence of a $_POST variable and return else:
function getPostValue($key) {
return isset($_POST[$key]) ? $_POST[$key] : null;
}
function sendMail() {
if ('POST' !== $_SEVER['REQUEST_METHOD']) {
return '';
}
if ($_SESSION['token'] !== $_POST['token']) {
return '0';
}
$_SESSION['token'] = '';
$name = getPostValue('name');
$email = getPostValue('email');
$message = getPostValue('message');
$number = getPostValue('number');
$date = getPostValue('date');
$select = getPostValue('select');
$radio = getPostValue('radio');
$checkbox = getPostValue('checkbox');
if (!isset($name, $email, $message, $number, $date, $select, $radio, $checkbox)) {
$response = "Error";
}
}-
All those input validations go with early return again (snippet only):
$spam_pattern = "/[\r\n]|Content-Type:|Bcc:|Cc:/i";
if (!preg_match($spam_pattern, $name)
|| !preg_match($spam_pattern, $number)
|| !preg_match($spam_pattern, $date)
|| !preg_match($spam_pattern, $radio)
|| !preg_match($spam_pattern, $checkbox)
|| !preg_match($spam_pattern, $select)
|| !preg_match($spam_pattern, $message)) {
return 'Invalid Request!';
}-
The
$to variables and others you use as parameters for mail are only used once. Furthermore the $message variable is overwritten, removing the original content. Variable reuse makes stuff hard to track though. I just do this all as arguments in the mail function (snippet):$sendResult = mail(
'support@loaidesign.co.uk',
'New Message From: ' . $name,
"Name:
number: $number
date: $date
select: $select
radio: $radio
checkbox: $checkbox
Email: $email
Message: $message",
"MIME-Version: 1.0 r\n Content-type: text/html; charset=utf-8 \r\n From: '.$email . '\r\n"
);-
All those returns have their html-markup in common. We sure can extract this into a common helper:
function getResponse($message, $isError) {
return '' . $message . '';
}-
Final remarks & further points of improvement:
- Indention of three characters is unusual. Most times its two or four.
- This can of course be further refactored: Especially creating local variables from
$_POSTis a point of repetation. Maybe it could be handled as an array. That'd make mass-checking (preg_match) easier: just loop over all input variables.
- If desired, this could of course be moved into a dedicated mailer class
- The functions depend on the global state (
$_POST). A next step would be to pass the required input variables as argument (maybe an array :)). This would make this function reusable as well.
- Returning formated strings limits reusability to HTML output. Throwing expections or error codes puts this into the responsibility of the caller, not the callee.
My final result:
```
function getPostValue($key) {
return isset($_POST[$key]) ? $_POST[$key] : null;
}
function getResponse($message, $isError) {
return '' . $message . '';
}
function sendMail() {
if ('POST' !== $_SEVER['REQUEST_METHOD']) {
Code Snippets
function sendMail() {
// your code here.
}function sendMail() {
if ('POST' !== $_SEVER['REQUEST_METHOD']) {
return '';
}
if ($_SESSION['token'] !== $_POST['token']) {
return '0';
}
// remaining code
}function getPostValue($key) {
return isset($_POST[$key]) ? $_POST[$key] : null;
}
function sendMail() {
if ('POST' !== $_SEVER['REQUEST_METHOD']) {
return '';
}
if ($_SESSION['token'] !== $_POST['token']) {
return '0';
}
$_SESSION['token'] = '';
$name = getPostValue('name');
$email = getPostValue('email');
$message = getPostValue('message');
$number = getPostValue('number');
$date = getPostValue('date');
$select = getPostValue('select');
$radio = getPostValue('radio');
$checkbox = getPostValue('checkbox');
if (!isset($name, $email, $message, $number, $date, $select, $radio, $checkbox)) {
$response = "<b style='color: red'>Error</b>";
}
}$spam_pattern = "/[\r\n]|Content-Type:|Bcc:|Cc:/i";
if (!preg_match($spam_pattern, $name)
|| !preg_match($spam_pattern, $number)
|| !preg_match($spam_pattern, $date)
|| !preg_match($spam_pattern, $radio)
|| !preg_match($spam_pattern, $checkbox)
|| !preg_match($spam_pattern, $select)
|| !preg_match($spam_pattern, $message)) {
return '<b style="color: red">Invalid Request!</b>';
}$sendResult = mail(
'support@loaidesign.co.uk',
'New Message From: ' . $name,
"Name: <br/>
number: $number<br/>
date: $date<br/>
select: $select<br/>
radio: $radio<br/>
checkbox: $checkbox<br/>
Email: $email<br/>
Message: $message",
"MIME-Version: 1.0 r\n Content-type: text/html; charset=utf-8 \r\n From: '.$email . '\r\n"
);Context
StackExchange Code Review Q#42411, answer score: 4
Revisions (0)
No revisions yet.