Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cookie: add input validation #609

Merged
merged 5 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/Cookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

namespace WpOrg\Requests;

use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Iri;
use WpOrg\Requests\Response\Headers;
use WpOrg\Requests\Utility\CaseInsensitiveDictionary;
use WpOrg\Requests\Utility\InputValidator;

/**
* Cookie storage object
Expand Down Expand Up @@ -67,8 +69,36 @@ class Cookie {
* @param string $name
* @param string $value
* @param array|\WpOrg\Requests\Utility\CaseInsensitiveDictionary $attributes Associative array of attribute data
* @param array $flags
* @param int|null $reference_time
*
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $name argument is not a string.
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $value argument is not a string.
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $attributes argument is not an array or iterable object with array access.
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $flags argument is not an array.
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $reference_time argument is not an integer or null.
*/
public function __construct($name, $value, $attributes = array(), $flags = array(), $reference_time = null) {
if (is_string($name) === false) {
throw InvalidArgument::create(1, '$name', 'string', gettype($name));
}

if (is_string($value) === false) {
throw InvalidArgument::create(2, '$value', 'string', gettype($value));
}

if (InputValidator::has_array_access($attributes) === false || InputValidator::is_iterable($attributes) === false) {
throw InvalidArgument::create(3, '$attributes', 'array|ArrayAccess&Traversable', gettype($attributes));
}

if (is_array($flags) === false) {
throw InvalidArgument::create(4, '$flags', 'array', gettype($flags));
}

if ($reference_time !== null && is_int($reference_time) === false) {
throw InvalidArgument::create(5, '$reference_time', 'integer|null', gettype($reference_time));
}

$this->name = $name;
$this->value = $value;
$this->attributes = $attributes;
Expand Down Expand Up @@ -148,6 +178,10 @@ public function uri_matches(Iri $uri) {
* @return boolean Whether the cookie is valid for the given domain
*/
public function domain_matches($domain) {
if (is_string($domain) === false) {
return false;
}

if (!isset($this->attributes['domain'])) {
// Cookies created manually; cookies created by Requests will set
// the domain to the requested domain
Expand Down Expand Up @@ -208,6 +242,10 @@ public function path_matches($request_path) {
return true;
}

if (is_scalar($request_path) === false) {
return false;
}

$cookie_path = $this->attributes['path'];

if ($cookie_path === $request_path) {
Expand Down Expand Up @@ -364,9 +402,22 @@ public function format_for_set_cookie() {
* specifies some of this handling, but not in a thorough manner.
*
* @param string $cookie_header Cookie header value (from a Set-Cookie header)
* @param string $name
* @param int|null $reference_time
* @return \WpOrg\Requests\Cookie Parsed cookie object
*
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $cookie_header argument is not a string.
* @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $name argument is not a string.
*/
public static function parse($cookie_header, $name = '', $reference_time = null) {
if (is_string($cookie_header) === false) {
throw InvalidArgument::create(1, '$cookie_header', 'string', gettype($cookie_header));
}

if (is_string($name) === false) {
throw InvalidArgument::create(2, '$name', 'string', gettype($name));
}

$parts = explode(';', $cookie_header);
$kvparts = array_shift($parts);

Expand Down
226 changes: 224 additions & 2 deletions tests/CookiesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

namespace WpOrg\Requests\Tests;

use DateTime;
use EmptyIterator;
use WpOrg\Requests\Cookie;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Iri;
use WpOrg\Requests\Requests;
use WpOrg\Requests\Response\Headers;
use WpOrg\Requests\Tests\Fixtures\ArrayAccessibleObject;
use WpOrg\Requests\Tests\Fixtures\StringableObject;
use WpOrg\Requests\Tests\TestCase;
use WpOrg\Requests\Utility\CaseInsensitiveDictionary;

Expand Down Expand Up @@ -125,6 +130,8 @@ public function testSendingMultipleCookies() {

public function domainMatchProvider() {
return array(
'Invalid check domain (type): null' => array('example.com', null, false, false),
'Invalid check domain (type): boolean true' => array('example.com', true, false, false),
array('example.com', 'example.com', true, true),
array('example.com', 'www.example.com', false, true),
array('example.com', 'example.net', false, false),
Expand Down Expand Up @@ -173,6 +180,10 @@ public function testDomainMatch($original, $check, $matches, $domain_matches) {

public function pathMatchProvider() {
return array(
'Invalid check path (type): null' => array('/', null, true),
'Invalid check path (type): true' => array('/', true, false),
'Invalid check path (type): integer' => array('/', 123, false),
'Invalid check path (type): array' => array('/', array(1, 2), false),
array('/', '', true),
array('/', '/', true),

Expand Down Expand Up @@ -411,7 +422,7 @@ public function testParsingHeader($header, $expected, $expected_attributes = arr
// Set the reference time to 2014-01-01 00:00:00
$reference_time = gmmktime(0, 0, 0, 1, 1, 2014);

$cookie = Cookie::parse($header, null, $reference_time);
$cookie = Cookie::parse($header, '', $reference_time);
$this->check_parsed_cookie($cookie, $expected, $expected_attributes);
}

Expand All @@ -424,7 +435,7 @@ public function testParsingHeaderDouble($header, $expected, $expected_attributes
// Set the reference time to 2014-01-01 00:00:00
$reference_time = gmmktime(0, 0, 0, 1, 1, 2014);

$cookie = Cookie::parse($header, null, $reference_time);
$cookie = Cookie::parse($header, '', $reference_time);

// Normalize the value again
$cookie->normalize();
Expand Down Expand Up @@ -580,4 +591,215 @@ public function testParsingHeaderWithOrigin($header, $origin, $expected, $expect
$cookie = reset($parsed);
$this->check_parsed_cookie($cookie, $expected, $expected_attributes, $expected_flags);
}

/**
* Tests receiving an exception when the constructor received an invalid input type as `$name`.
*
* @dataProvider dataInvalidStringInput
*
* @covers \WpOrg\Requests\Cookie::__construct
*
* @param mixed $input Invalid parameter input.
*
* @return void
*/
public function testConstructorInvalidName($input) {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #1 ($name) must be of type string');

new Cookie($input, 'value');
}

/**
* Tests receiving an exception when the constructor received an invalid input type as `$value`.
*
* @dataProvider dataInvalidStringInput
*
* @covers \WpOrg\Requests\Cookie::__construct
*
* @param mixed $input Invalid parameter input.
*
* @return void
*/
public function testConstructorInvalidValue($input) {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #2 ($value) must be of type string');

new Cookie('name', $input);
}

/**
* Data Provider.
*
* @return array
*/
public function dataInvalidStringInput() {
return array(
'null' => array(null),
'float' => array(1.1),
'stringable object' => array(new StringableObject('name')),
);
}

/**
* Tests receiving an exception when the constructor received an invalid input type as `$name`.
*
* @dataProvider dataConstructorInvalidAttributes
*
* @covers \WpOrg\Requests\Cookie::__construct
*
* @param mixed $input Invalid parameter input.
*
* @return void
*/
public function testConstructorInvalidAttributes($input) {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #3 ($attributes) must be of type array|ArrayAccess&Traversable');

new Cookie('name', 'value', $input);
}

/**
* Data Provider.
*
* @return array
*/
public function dataConstructorInvalidAttributes() {
return array(
'null' => array(null),
'text string' => array('array'),
'iterator object without array access' => array(new EmptyIterator()),
'array accessible object not iterable' => array(new ArrayAccessibleObject(array(1, 2, 3))),
);
}

/**
* Tests receiving an exception when the constructor received an invalid input type as `$flags`.
*
* @dataProvider dataConstructorInvalidFlags
*
* @covers \WpOrg\Requests\Cookie::__construct
*
* @param mixed $input Invalid parameter input.
*
* @return void
*/
public function testConstructorInvalidFlags($input) {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #4 ($flags) must be of type array');

new Cookie('name', 'value', array(), $input);
}

/**
* Data Provider.
*
* @return array
*/
public function dataConstructorInvalidFlags() {
return array(
'null' => array(null),
'integer' => array(101),
'array accessible object' => array(new ArrayAccessibleObject(array())),
);
}

/**
* Tests receiving an exception when the constructor received an invalid input type as `$reference_time`.
*
* @dataProvider dataConstructorInvalidReferenceTime
*
* @covers \WpOrg\Requests\Cookie::__construct
*
* @param mixed $input Invalid parameter input.
*
* @return void
*/
public function testConstructorInvalidReferenceTime($input) {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #5 ($reference_time) must be of type integer|null');

new Cookie('name', 'value', array(), array(), $input);
}

/**
* Data Provider.
*
* @return array
*/
public function dataConstructorInvalidReferenceTime() {
return array(
'float' => array(1.1),
'string' => array('now'),
'DateTime object' => array(new DateTime('now')),
);
}

/**
* Tests receiving an exception when the parse() method received an invalid input type as `$cookie_header`.
*
* @dataProvider dataInvalidStringInput
*
* @covers \WpOrg\Requests\Cookie::parse
*
* @param mixed $input Invalid parameter input.
*
* @return void
*/
public function testParseInvalidCookieHeader($input) {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #1 ($cookie_header) must be of type string');

Cookie::parse($input);
}

/**
* Tests receiving an exception when the parse() method received an invalid input type as `$name`.
*
* @dataProvider dataInvalidStringInput
*
* @covers \WpOrg\Requests\Cookie::parse
*
* @param mixed $input Invalid parameter input.
*
* @return void
*/
public function testParseInvalidName($input) {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #2 ($name) must be of type string');

Cookie::parse('test', $input);
}

/**
* Tests receiving an exception when the parse() method received an invalid input type as `$reference_time`.
*
* @covers \WpOrg\Requests\Cookie::parse
*
* @return void
*/
public function testParseInvalidReferenceTime() {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #5 ($reference_time) must be of type integer|null');

Cookie::parse('test', 'test', 'now');
}

/**
* Tests receiving an exception when the parse_from_headers() method received an invalid input type as `$reference_time`.
*
* @covers \WpOrg\Requests\Cookie::parse_from_headers
*
* @return void
*/
public function testParseFromHeadersInvalidReferenceTime() {
$this->expectException(InvalidArgument::class);
$this->expectExceptionMessage('Argument #5 ($reference_time) must be of type integer|null');

$origin = new Iri();
$headers = new Headers();
$headers['Set-Cookie'] = 'name=value;';

Cookie::parse_from_headers($headers, $origin, 'now');
}
}