Skip to content
This repository was archived by the owner on Oct 2, 2019. It is now read-only.

Commit b0490b4

Browse files
committed
[ZF2015-04] Fix CRLF injections in HTTP and Mail
This patch mirrors that made in ZF2 to address ZF2015-04. It adds the following classes: - `Zend_Http_Header_HeaderValue`, which provides functionality for validating, filtering, and asserting that header values follow RFC 2822. - `Zend_Mail_Header_HeaderName`, which provides functionality for validating, filtering, and asserting that header names follow RFC 2822. - `Zend_Mail_Header_HeaderValue`, which provides functionality for validating, filtering, and asserting that header values follow RFC 7230. The following specific changes were made to existing functionality: - `Zend_Mail_Part::__construct()` was modified in order to validate mail headers provided to it. - `Zend_Http_Header_SetCookie`'s `setName()`, `setValue()`, `setDomain()`, and `setPath()` methods were modified to validate incoming values. - `Zend_Http_Response::extractHeaders()` was modified to follow RFC 7230 and only split on `\r\n` sequences when splitting header lines. Each value extracted is tested for validity. - `Zend_Http_Response::extractBody()` was modified to follow RFC 7230 and only split on `\r\n` sequences when splitting the message from the headers. - `Zend_Http_Client::setHeaders()` was modified to validate incoming header values.
1 parent 712b7ec commit b0490b4

20 files changed

+1072
-68
lines changed

library/Zend/Http/Client.php

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@
3939
require_once 'Zend/Http/Client/Adapter/Interface.php';
4040

4141

42+
/**
43+
* @see Zend_Http_Header_HeaderValue
44+
*/
45+
require_once 'Zend/Http/Header/HeaderValue.php';
46+
47+
4248
/**
4349
* @see Zend_Http_Response
4450
*/
@@ -431,38 +437,40 @@ public function setHeaders($name, $value = null)
431437
foreach ($name as $k => $v) {
432438
if (is_string($k)) {
433439
$this->setHeaders($k, $v);
434-
} else {
435-
$this->setHeaders($v, null);
440+
continue;
436441
}
442+
$this->setHeaders($v, null);
437443
}
438-
} else {
439-
// Check if $name needs to be split
440-
if ($value === null && (strpos($name, ':') > 0)) {
441-
list($name, $value) = explode(':', $name, 2);
442-
}
444+
return $this;
445+
}
443446

444-
// Make sure the name is valid if we are in strict mode
445-
if ($this->config['strict'] && (! preg_match('/^[a-zA-Z0-9-]+$/', $name))) {
446-
/** @see Zend_Http_Client_Exception */
447-
require_once 'Zend/Http/Client/Exception.php';
448-
throw new Zend_Http_Client_Exception("{$name} is not a valid HTTP header name");
449-
}
447+
// Check if $name needs to be split
448+
if ($value === null && (strpos($name, ':') > 0)) {
449+
list($name, $value) = explode(':', $name, 2);
450+
}
450451

451-
$normalized_name = strtolower($name);
452+
// Make sure the name is valid if we are in strict mode
453+
if ($this->config['strict'] && (! preg_match('/^[a-zA-Z0-9-]+$/', $name))) {
454+
require_once 'Zend/Http/Client/Exception.php';
455+
throw new Zend_Http_Client_Exception("{$name} is not a valid HTTP header name");
456+
}
452457

453-
// If $value is null or false, unset the header
454-
if ($value === null || $value === false) {
455-
unset($this->headers[$normalized_name]);
458+
$normalized_name = strtolower($name);
456459

457-
// Else, set the header
458-
} else {
459-
// Header names are stored lowercase internally.
460-
if (is_string($value)) {
461-
$value = trim($value);
462-
}
463-
$this->headers[$normalized_name] = array($name, $value);
464-
}
460+
// If $value is null or false, unset the header
461+
if ($value === null || $value === false) {
462+
unset($this->headers[$normalized_name]);
463+
return $this;
464+
}
465+
466+
// Validate value
467+
$this->_validateHeaderValue($value);
468+
469+
// Header names are stored lowercase internally.
470+
if (is_string($value)) {
471+
$value = trim($value);
465472
}
473+
$this->headers[$normalized_name] = array($name, $value);
466474

467475
return $this;
468476
}
@@ -1568,4 +1576,27 @@ protected static function _flattenParametersArray($parray, $prefix = null)
15681576
return $parameters;
15691577
}
15701578

1579+
/**
1580+
* Ensure a header value is valid per RFC 7230.
1581+
*
1582+
* @see http://tools.ietf.org/html/rfc7230#section-3.2
1583+
* @param string|object|array $value
1584+
* @param bool $recurse
1585+
*/
1586+
protected function _validateHeaderValue($value, $recurse = true)
1587+
{
1588+
if (is_array($value) && $recurse) {
1589+
foreach ($value as $v) {
1590+
$this->_validateHeaderValue($v, false);
1591+
}
1592+
return;
1593+
}
1594+
1595+
if (! is_string($value) && (! is_object($value) || ! method_exists($value, '__toString'))) {
1596+
require_once 'Zend/Http/Exception.php';
1597+
throw new Zend_Http_Exception('Invalid header value detected');
1598+
}
1599+
1600+
Zend_Http_Header_HeaderValue::assertValid($value);
1601+
}
15711602
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
/**
3+
* Zend Framework
4+
*
5+
* LICENSE
6+
*
7+
* This source file is subject to the new BSD license that is bundled
8+
* with this package in the file LICENSE.txt.
9+
* It is also available through the world-wide-web at this URL:
10+
* http://framework.zend.com/license/new-bsd
11+
* If you did not receive a copy of the license and are unable to
12+
* obtain it through the world-wide-web, please send an email
13+
* to [email protected] so we can send you a copy immediately.
14+
*
15+
* @category Zend
16+
* @package Zend_Http
17+
* @subpackage Header
18+
* @version $Id$
19+
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
20+
* @license http://framework.zend.com/license/new-bsd New BSD License
21+
*/
22+
23+
24+
/**
25+
* @category Zend
26+
* @package Zend_Http
27+
* @subpackage Header
28+
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
29+
* @license http://framework.zend.com/license/new-bsd New BSD License
30+
*/
31+
final class Zend_Http_Header_HeaderValue
32+
{
33+
/**
34+
* Private constructor; non-instantiable.
35+
*/
36+
private function __construct()
37+
{
38+
}
39+
40+
/**
41+
* Filter a header value
42+
*
43+
* Ensures CRLF header injection vectors are filtered.
44+
*
45+
* Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
46+
* tabs are allowed in values; only one whitespace character is allowed
47+
* between visible characters.
48+
*
49+
* @see http://en.wikipedia.org/wiki/HTTP_response_splitting
50+
* @param string $value
51+
* @return string
52+
*/
53+
public static function filter($value)
54+
{
55+
$value = (string) $value;
56+
$length = strlen($value);
57+
$string = '';
58+
for ($i = 0; $i < $length; $i += 1) {
59+
$ascii = ord($value[$i]);
60+
61+
// Non-visible, non-whitespace characters
62+
// 9 === horizontal tab
63+
// 32-126, 128-254 === visible
64+
// 127 === DEL
65+
// 255 === null byte
66+
if (($ascii < 32 && $ascii !== 9)
67+
|| $ascii === 127
68+
|| $ascii > 254
69+
) {
70+
continue;
71+
}
72+
73+
$string .= $value[$i];
74+
}
75+
76+
return $string;
77+
}
78+
79+
/**
80+
* Validate a header value.
81+
*
82+
* Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
83+
* tabs are allowed in values; only one whitespace character is allowed
84+
* between visible characters.
85+
*
86+
* @see http://en.wikipedia.org/wiki/HTTP_response_splitting
87+
* @param string $value
88+
* @return bool
89+
*/
90+
public static function isValid($value)
91+
{
92+
$value = (string) $value;
93+
$length = strlen($value);
94+
for ($i = 0; $i < $length; $i += 1) {
95+
$ascii = ord($value[$i]);
96+
97+
// Non-visible, non-whitespace characters
98+
// 9 === horizontal tab
99+
// 32-126, 128-254 === visible
100+
// 127 === DEL
101+
// 255 === null byte
102+
if (($ascii < 32 && $ascii !== 9)
103+
|| $ascii === 127
104+
|| $ascii > 254
105+
) {
106+
return false;
107+
}
108+
}
109+
110+
return true;
111+
}
112+
113+
/**
114+
* Assert a header value is valid.
115+
*
116+
* @param string $value
117+
* @throws Exception\RuntimeException for invalid values
118+
* @return void
119+
*/
120+
public static function assertValid($value)
121+
{
122+
if (! self::isValid($value)) {
123+
require_once 'Zend/Http/Header/Exception/InvalidArgumentException.php';
124+
throw new Zend_Http_Header_Exception_InvalidArgumentException('Invalid header value');
125+
}
126+
}
127+
}

library/Zend/Http/Header/SetCookie.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
*/
3232
require_once "Zend/Http/Header/Exception/RuntimeException.php";
3333

34+
/**
35+
* @see Zend_Http_Header_HeaderValue
36+
*/
37+
require_once "Zend/Http/Header/HeaderValue.php";
38+
3439
/**
3540
* Zend_Http_Client is an implementation of an HTTP client in PHP. The client
3641
* supports basic features like sending different HTTP requests and handling
@@ -311,6 +316,7 @@ public function getName()
311316
*/
312317
public function setValue($value)
313318
{
319+
Zend_Http_Header_HeaderValue::assertValid($value);
314320
$this->value = $value;
315321
return $this;
316322
}
@@ -405,6 +411,7 @@ public function getExpires($inSeconds = false)
405411
*/
406412
public function setDomain($domain)
407413
{
414+
Zend_Http_Header_HeaderValue::assertValid($domain);
408415
$this->domain = $domain;
409416
return $this;
410417
}
@@ -422,6 +429,7 @@ public function getDomain()
422429
*/
423430
public function setPath($path)
424431
{
432+
Zend_Http_Header_HeaderValue::assertValid($path);
425433
$this->path = $path;
426434
return $this;
427435
}

library/Zend/Http/Response.php

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
* @license http://framework.zend.com/license/new-bsd New BSD License
2222
*/
2323

24+
/**
25+
* @see Zend_Http_Header_HeaderValue
26+
*/
27+
require_once 'Zend/Http/Header/HeaderValue.php';
28+
2429
/**
2530
* Zend_Http_Response represents an HTTP 1.0 / 1.1 response message. It
2631
* includes easy access to all the response's different elemts, as well as some
@@ -394,7 +399,7 @@ public function getHeadersAsString($status_line = true, $br = "\n")
394399
* @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
395400
* @return string
396401
*/
397-
public function asString($br = "\n")
402+
public function asString($br = "\r\n")
398403
{
399404
return $this->getHeadersAsString(true, $br) . $br . $this->getRawBody();
400405
}
@@ -496,44 +501,75 @@ public static function extractHeaders($response_str)
496501
{
497502
$headers = array();
498503

499-
// First, split body and headers
500-
$parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2);
501-
if (! $parts[0]) return $headers;
504+
// First, split body and headers. Headers are separated from the
505+
// message at exactly the sequence "\r\n\r\n"
506+
$parts = preg_split('|(?:\r\n){2}|m', $response_str, 2);
507+
if (! $parts[0]) {
508+
return $headers;
509+
}
502510

503-
// Split headers part to lines
504-
$lines = explode("\n", $parts[0]);
511+
// Split headers part to lines; "\r\n" is the only valid line separator.
512+
$lines = explode("\r\n", $parts[0]);
505513
unset($parts);
506514
$last_header = null;
507515

508-
foreach($lines as $line) {
509-
$line = trim($line, "\r\n");
510-
if ($line == "") break;
516+
foreach($lines as $index => $line) {
517+
if ($index === 0 && preg_match('#^HTTP/\d+(?:\.\d+) [1-5]\d+#', $line)) {
518+
// Status line; ignore
519+
continue;
520+
}
521+
522+
if ($line == "") {
523+
// Done processing headers
524+
break;
525+
}
511526

512527
// Locate headers like 'Location: ...' and 'Location:...' (note the missing space)
513-
if (preg_match("|^([\w-]+):\s*(.+)|", $line, $m)) {
528+
if (preg_match("|^([\w-]+):\s*(.+)|s", $line, $m)) {
514529
unset($last_header);
515-
$h_name = strtolower($m[1]);
530+
$h_name = strtolower($m[1]);
516531
$h_value = $m[2];
532+
Zend_Http_Header_HeaderValue::assertValid($h_value);
517533

518534
if (isset($headers[$h_name])) {
519535
if (! is_array($headers[$h_name])) {
520536
$headers[$h_name] = array($headers[$h_name]);
521537
}
522538

523539
$headers[$h_name][] = $h_value;
524-
} else {
525-
$headers[$h_name] = $h_value;
540+
$last_header = $h_name;
541+
continue;
526542
}
543+
544+
$headers[$h_name] = $h_value;
527545
$last_header = $h_name;
528-
} elseif (preg_match("|^\s+(.+)$|", $line, $m) && $last_header !== null) {
546+
continue;
547+
}
548+
549+
// Identify header continuations
550+
if (preg_match("|^[ \t](.+)$|s", $line, $m) && $last_header !== null) {
551+
$h_value = trim($m[1]);
529552
if (is_array($headers[$last_header])) {
530553
end($headers[$last_header]);
531554
$last_header_key = key($headers[$last_header]);
532-
$headers[$last_header][$last_header_key] .= $m[1];
533-
} else {
534-
$headers[$last_header] .= $m[1];
555+
556+
$h_value = $headers[$last_header][$last_header_key] . $h_value;
557+
Zend_Http_Header_HeaderValue::assertValid($h_value);
558+
559+
$headers[$last_header][$last_header_key] = $h_value;
560+
continue;
535561
}
562+
563+
$h_value = $headers[$last_header] . $h_value;
564+
Zend_Http_Header_HeaderValue::assertValid($h_value);
565+
566+
$headers[$last_header] = $h_value;
567+
continue;
536568
}
569+
570+
// Anything else is an error condition
571+
require_once 'Zend/Http/Exception.php';
572+
throw new Zend_Http_Exception('Invalid header line detected');
537573
}
538574

539575
return $headers;
@@ -547,7 +583,7 @@ public static function extractHeaders($response_str)
547583
*/
548584
public static function extractBody($response_str)
549585
{
550-
$parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2);
586+
$parts = preg_split('|(?:\r\n){2}|m', $response_str, 2);
551587
if (isset($parts[1])) {
552588
return $parts[1];
553589
}

0 commit comments

Comments
 (0)