Skip to content

Commit

Permalink
Implemented Bug #68776 mail() does not have mail header injection pre…
Browse files Browse the repository at this point in the history
…vention for additional headers

(PR 2060)
  • Loading branch information
Yasuo Ohgaki committed Sep 14, 2016
1 parent dbdc7cb commit 6e53050
Show file tree
Hide file tree
Showing 6 changed files with 498 additions and 20 deletions.
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ PHP NEWS
- Core:
. Removed the sql.safe_mode directive. (Kalle)
. Fixed bug #54535 (WSA cleanup executes before MSHUTDOWN). (Kalle)
. Implemented bug #68776 (mail() does not have mail header injection prevention
for additional headers) (Yasuo)

- EXIF:
. Added support for vendor specific tags for the following formats:
Expand Down
8 changes: 8 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ PHP 7.2 UPGRADE NOTES
'PASSWORD_ARGON2_DEFAULT_TIME_COST', and 'PASSWORD_ARGON2_DEFAULT_THREADS' respectively if not set.
. password_verify() can verify Argon2i hashes.
. password_get_info() and password_needs_rehash() can accept Argon2i hashes.
. mail()/mb_send_mail() accept array $extra_header. Array paramter is checked against RFC 2822.
Array format is
$extra_headers = [
'Header-Name' => 'Header value',
'Multiple' => ['One header', 'Another header'],
'Multiline' = "FirstLine\r\n SecondLine",
];


========================================
6. New Functions
Expand Down
41 changes: 29 additions & 12 deletions ext/mbstring/mbstring.c
Original file line number Diff line number Diff line change
Expand Up @@ -4392,11 +4392,11 @@ PHP_FUNCTION(mb_send_mail)
size_t to_len;
char *message = NULL;
size_t message_len;
char *headers = NULL;
size_t headers_len;
char *subject = NULL;
zend_string *extra_cmd = NULL;
size_t subject_len;
zval *headers = NULL;
zend_string *extra_cmd = NULL;
zend_string *str_headers=NULL, *tmp_headers;
int i;
char *to_r = NULL;
char *force_extra_parameters = INI_STR("mail.force_extra_parameters");
Expand Down Expand Up @@ -4436,7 +4436,7 @@ PHP_FUNCTION(mb_send_mail)
body_enc = lang->mail_body_encoding;
}

if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss|sS", &to, &to_len, &subject, &subject_len, &message, &message_len, &headers, &headers_len, &extra_cmd) == FAILURE) {
if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss|zS", &to, &to_len, &subject, &subject_len, &message, &message_len, &headers, &extra_cmd) == FAILURE) {
return;
}

Expand All @@ -4445,16 +4445,29 @@ PHP_FUNCTION(mb_send_mail)
MAIL_ASCIIZ_CHECK_MBSTRING(subject, subject_len);
MAIL_ASCIIZ_CHECK_MBSTRING(message, message_len);
if (headers) {
MAIL_ASCIIZ_CHECK_MBSTRING(headers, headers_len);
switch(Z_TYPE_P(headers)) {
case IS_STRING:
tmp_headers = zend_string_init(Z_STRVAL_P(headers), Z_STRLEN_P(headers), 0);
MAIL_ASCIIZ_CHECK_MBSTRING(ZSTR_VAL(tmp_headers), ZSTR_LEN(tmp_headers));
str_headers = php_trim(tmp_headers, NULL, 0, 2);
zend_string_release(tmp_headers);
break;
case IS_ARRAY:
str_headers = php_mail_build_headers(headers);
break;
default:
php_error_docref(NULL, E_WARNING, "headers parameter must be string or array");
RETURN_FALSE;
}
}
if (extra_cmd) {
MAIL_ASCIIZ_CHECK_MBSTRING(ZSTR_VAL(extra_cmd), ZSTR_LEN(extra_cmd));
}

zend_hash_init(&ht_headers, 0, NULL, ZVAL_PTR_DTOR, 0);

if (headers != NULL) {
_php_mbstr_parse_mail_headers(&ht_headers, headers, headers_len);
if (str_headers != NULL) {
_php_mbstr_parse_mail_headers(&ht_headers, ZSTR_VAL(str_headers), ZSTR_LEN(str_headers));
}

if ((s = zend_hash_str_find(&ht_headers, "CONTENT-TYPE", sizeof("CONTENT-TYPE") - 1))) {
Expand Down Expand Up @@ -4597,10 +4610,11 @@ PHP_FUNCTION(mb_send_mail)
#define PHP_MBSTR_MAIL_MIME_HEADER2 "Content-Type: text/plain"
#define PHP_MBSTR_MAIL_MIME_HEADER3 "; charset="
#define PHP_MBSTR_MAIL_MIME_HEADER4 "Content-Transfer-Encoding: "
if (headers != NULL) {
p = headers;
n = headers_len;
if (str_headers != NULL) {
p = ZSTR_VAL(str_headers);
n = ZSTR_LEN(str_headers);
mbfl_memory_device_strncat(&device, p, n);
zend_string_release(str_headers);
if (n > 0 && p[n - 1] != '\n') {
mbfl_memory_device_strncat(&device, "\n", 1);
}
Expand Down Expand Up @@ -4633,15 +4647,15 @@ PHP_FUNCTION(mb_send_mail)

mbfl_memory_device_unput(&device);
mbfl_memory_device_output('\0', &device);
headers = (char *)device.buffer;
str_headers = zend_string_init((char *)device.buffer, strlen((char *)device.buffer), 0);

if (force_extra_parameters) {
extra_cmd = php_escape_shell_cmd(force_extra_parameters);
} else if (extra_cmd) {
extra_cmd = php_escape_shell_cmd(ZSTR_VAL(extra_cmd));
}

if (!err && php_mail(to_r, subject, message, headers, extra_cmd ? ZSTR_VAL(extra_cmd) : NULL)) {
if (!err && php_mail(to_r, subject, message, ZSTR_VAL(str_headers), extra_cmd ? ZSTR_VAL(extra_cmd) : NULL)) {
RETVAL_TRUE;
} else {
RETVAL_FALSE;
Expand All @@ -4662,6 +4676,9 @@ PHP_FUNCTION(mb_send_mail)
}
mbfl_memory_device_clear(&device);
zend_hash_destroy(&ht_headers);
if (str_headers) {
zend_string_release(str_headers);
}
}

#undef SKIP_LONG_HEADER_SEP_MBSTRING
Expand Down
217 changes: 210 additions & 7 deletions ext/standard/mail.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "ext/standard/php_string.h"
#include "ext/standard/basic_functions.h"
#include "ext/date/php_date.h"
#include "zend_smart_str.h"

#if HAVE_SYSEXITS_H
#include <sysexits.h>
Expand Down Expand Up @@ -96,20 +97,210 @@ PHP_FUNCTION(ezmlm_hash)
}
/* }}} */


static zend_bool php_mail_build_headers_check_field_value(zval *val)
{
size_t len = 0;
zend_string *value = Z_STR_P(val);

/* https://tools.ietf.org/html/rfc2822#section-2.2.1 */
/* https://tools.ietf.org/html/rfc2822#section-2.2.3 */
while (len < value->len) {
if (*(value->val+len) == '\r') {
if (value->len - len >= 3
&& *(value->val+len+1) == '\n'
&& (*(value->val+len+2) == ' ' || *(value->val+len+2) == '\t')) {
len += 3;
continue;
}
return FAILURE;
}
if (*(value->val+len) == '\0') {
return FAILURE;
}
len++;
}
return SUCCESS;
}


static zend_bool php_mail_build_headers_check_field_name(zend_string *key)
{
size_t len = 0;

/* https://tools.ietf.org/html/rfc2822#section-2.2 */
while (len < key->len) {
if (*(key->val+len) < 33 || *(key->val+len) > 126 || *(key->val+len) == ':') {
return FAILURE;
}
len++;
}
return SUCCESS;
}


static void php_mail_build_headers_elems(smart_str *s, zend_string *key, zval *val);

static void php_mail_build_headers_elem(smart_str *s, zend_string *key, zval *val)
{
switch(Z_TYPE_P(val)) {
case IS_STRING:
if (php_mail_build_headers_check_field_name(key) != SUCCESS) {
php_error_docref(NULL, E_WARNING, "Header field name (%s) contains invalid chars", ZSTR_VAL(key));
return;
}
if (php_mail_build_headers_check_field_value(val) != SUCCESS) {
php_error_docref(NULL, E_WARNING, "Header field value (%s => %s) contains invalid chars or format", ZSTR_VAL(key), Z_STRVAL_P(val));
return;
}
smart_str_append(s, key);
smart_str_appendl(s, ": ", 2);
smart_str_appends(s, Z_STRVAL_P(val));
smart_str_appendl(s, "\r\n", 2);
break;
case IS_ARRAY:
php_mail_build_headers_elems(s, key, val);
break;
default:
php_error_docref(NULL, E_WARNING, "headers array elements must be string or array (%s)", ZSTR_VAL(key));
}
}


static void php_mail_build_headers_elems(smart_str *s, zend_string *key, zval *val)
{
zend_ulong idx;
zend_string *tmp_key;
zval *tmp_val;

(void)(idx);
ZEND_HASH_FOREACH_KEY_VAL(HASH_OF(val), idx, tmp_key, tmp_val) {
if (tmp_key) {
php_error_docref(NULL, E_WARNING, "Multiple header key must be numeric index (%s)", ZSTR_VAL(tmp_key));
continue;
}
if (Z_TYPE_P(tmp_val) != IS_STRING) {
php_error_docref(NULL, E_WARNING, "Multiple header values must be string (%s)", ZSTR_VAL(key));
continue;
}
php_mail_build_headers_elem(s, key, tmp_val);
} ZEND_HASH_FOREACH_END();
}


PHPAPI zend_string *php_mail_build_headers(zval *headers)
{
zend_ulong idx;
zend_string *key;
zval *val;
smart_str s = {0};

ZEND_ASSERT(Z_TYPE_P(headers) == IS_ARRAY);

ZEND_HASH_FOREACH_KEY_VAL(HASH_OF(headers), idx, key, val) {
if (!key) {
php_error_docref(NULL, E_WARNING, "Found numeric header (" ZEND_LONG_FMT ")", idx);
continue;
}
/* https://tools.ietf.org/html/rfc2822#section-3.6 */
switch(ZSTR_LEN(key)) {
case sizeof("orig-date")-1:
if (!strncasecmp("orig-date", ZSTR_VAL(key), ZSTR_LEN(key))) {
PHP_MAIL_BUILD_HEADER_CHECK("orig-date", s, key, val);
} else {
PHP_MAIL_BUILD_HEADER_DEFAULT(s, key, val);
}
break;
case sizeof("from")-1:
if (!strncasecmp("from", ZSTR_VAL(key), ZSTR_LEN(key))) {
PHP_MAIL_BUILD_HEADER_CHECK("from", s, key, val);
} else {
PHP_MAIL_BUILD_HEADER_DEFAULT(s, key, val);
}
break;
case sizeof("sender")-1:
if (!strncasecmp("sender", ZSTR_VAL(key), ZSTR_LEN(key))) {
PHP_MAIL_BUILD_HEADER_CHECK("sender", s, key, val);
} else {
PHP_MAIL_BUILD_HEADER_DEFAULT(s, key, val);
}
break;
case sizeof("reply-to")-1:
if (!strncasecmp("reply-to", ZSTR_VAL(key), ZSTR_LEN(key))) {
PHP_MAIL_BUILD_HEADER_CHECK("reply-to", s, key, val);
} else {
PHP_MAIL_BUILD_HEADER_DEFAULT(s, key, val);
}
break;
case sizeof("to")-1: /* "to", "cc" */
if (!strncasecmp("to", ZSTR_VAL(key), ZSTR_LEN(key))) {
php_error_docref(NULL, E_WARNING, "Extra header cannot contain 'To' header");
continue;
}
if (!strncasecmp("cc", ZSTR_VAL(key), ZSTR_LEN(key))) {
PHP_MAIL_BUILD_HEADER_CHECK("cc", s, key, val);
} else {
PHP_MAIL_BUILD_HEADER_DEFAULT(s, key, val);
}
break;
case sizeof("bcc")-1:
if (!strncasecmp("bcc", ZSTR_VAL(key), ZSTR_LEN(key))) {
PHP_MAIL_BUILD_HEADER_CHECK("bcc", s, key, val);
} else {
PHP_MAIL_BUILD_HEADER_DEFAULT(s, key, val);
}
break;
case sizeof("message-id")-1: /* "references" */
if (!strncasecmp("message-id", ZSTR_VAL(key), ZSTR_LEN(key))) {
PHP_MAIL_BUILD_HEADER_CHECK("message-id", s, key, val);
} else if (!strncasecmp("references", ZSTR_VAL(key), ZSTR_LEN(key))) {
PHP_MAIL_BUILD_HEADER_CHECK("references", s, key, val);
} else {
PHP_MAIL_BUILD_HEADER_DEFAULT(s, key, val);
}
break;
case sizeof("in-reply-to")-1:
if (!strncasecmp("in-reply-to", ZSTR_VAL(key), ZSTR_LEN(key))) {
PHP_MAIL_BUILD_HEADER_CHECK("in-reply-to", s, key, val);
} else {
PHP_MAIL_BUILD_HEADER_DEFAULT(s, key, val);
}
break;
case sizeof("subject")-1:
if (!strncasecmp("subject", ZSTR_VAL(key), ZSTR_LEN(key))) {
php_error_docref(NULL, E_WARNING, "Extra header cannot contain 'Subject' header");
continue;
}
PHP_MAIL_BUILD_HEADER_DEFAULT(s, key, val);
break;
default:
PHP_MAIL_BUILD_HEADER_DEFAULT(s, key, val);
}
} ZEND_HASH_FOREACH_END();

/* Remove the last \r\n */
if (s.s) s.s->len -= 2;
smart_str_0(&s);

return s.s;
}


/* {{{ proto int mail(string to, string subject, string message [, string additional_headers [, string additional_parameters]])
Send an email message */
PHP_FUNCTION(mail)
{
char *to=NULL, *message=NULL;
char *subject=NULL;
zend_string *extra_cmd=NULL, *headers=NULL, *headers_trimmed=NULL;
zend_string *extra_cmd=NULL, *str_headers=NULL, *tmp_headers;
zval *headers = NULL;
size_t to_len, message_len;
size_t subject_len, i;
char *force_extra_parameters = INI_STR("mail.force_extra_parameters");
char *to_r, *subject_r;
char *p, *e;

if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss|SS", &to, &to_len, &subject, &subject_len, &message, &message_len, &headers, &extra_cmd) == FAILURE) {
if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss|zS", &to, &to_len, &subject, &subject_len, &message, &message_len, &headers, &extra_cmd) == FAILURE) {
return;
}

Expand All @@ -118,8 +309,20 @@ PHP_FUNCTION(mail)
MAIL_ASCIIZ_CHECK(subject, subject_len);
MAIL_ASCIIZ_CHECK(message, message_len);
if (headers) {
MAIL_ASCIIZ_CHECK(ZSTR_VAL(headers), ZSTR_LEN(headers));
headers_trimmed = php_trim(headers, NULL, 0, 2);
switch(Z_TYPE_P(headers)) {
case IS_STRING:
tmp_headers = zend_string_init(Z_STRVAL_P(headers), Z_STRLEN_P(headers), 0);
MAIL_ASCIIZ_CHECK(ZSTR_VAL(tmp_headers), ZSTR_LEN(tmp_headers));
str_headers = php_trim(tmp_headers, NULL, 0, 2);
zend_string_release(tmp_headers);
break;
case IS_ARRAY:
str_headers = php_mail_build_headers(headers);
break;
default:
php_error_docref(NULL, E_WARNING, "headers parameter must be string or array");
RETURN_FALSE;
}
}
if (extra_cmd) {
MAIL_ASCIIZ_CHECK(ZSTR_VAL(extra_cmd), ZSTR_LEN(extra_cmd));
Expand Down Expand Up @@ -171,14 +374,14 @@ PHP_FUNCTION(mail)
extra_cmd = php_escape_shell_cmd(ZSTR_VAL(extra_cmd));
}

if (php_mail(to_r, subject_r, message, headers_trimmed ? ZSTR_VAL(headers_trimmed) : NULL, extra_cmd ? ZSTR_VAL(extra_cmd) : NULL)) {
if (php_mail(to_r, subject_r, message, str_headers ? ZSTR_VAL(str_headers) : NULL, extra_cmd ? ZSTR_VAL(extra_cmd) : NULL)) {
RETVAL_TRUE;
} else {
RETVAL_FALSE;
}

if (headers_trimmed) {
zend_string_release(headers_trimmed);
if (str_headers) {
zend_string_release(str_headers);
}

if (extra_cmd) {
Expand Down
Loading

0 comments on commit 6e53050

Please sign in to comment.