Skip to content

Commit e219300

Browse files
oakbanialiabbasrizvi
authored andcommitted
fix: escape shell arg before passing into curl body (#163)
1 parent 1ce4795 commit e219300

File tree

2 files changed

+157
-2
lines changed

2 files changed

+157
-2
lines changed

src/Optimizely/Event/Dispatcher/CurlEventDispatcher.php

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright 2016, Optimizely
3+
* Copyright 2016, 2019, Optimizely
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -27,14 +27,81 @@
2727
*/
2828
class CurlEventDispatcher implements EventDispatcherInterface
2929
{
30+
/**
31+
* Calls escapeshellarg on arg if arg is of type string.
32+
*
33+
* @param mixed $val arg to escape.
34+
*
35+
* @return mixed arg as it is or escaped if it's a string.
36+
*/
37+
protected function escapeIfString($val)
38+
{
39+
if (is_string($val)) {
40+
return escapeshellarg($val);
41+
}
42+
43+
return $val;
44+
}
45+
46+
/**
47+
* Escapes certain user provided values from event payload which includes
48+
* 1. user ID.
49+
* 2. attribute values of type string.
50+
* 3. event tag keys.
51+
* 4. event tag values of type string.
52+
*
53+
* @param array $params LogEvent params.
54+
*
55+
* @return array Escaped params.
56+
**/
57+
public function sanitizeEventPayload(array $params)
58+
{
59+
foreach ($params['visitors'] as &$visitor) {
60+
// user ID
61+
$visitor['visitor_id'] = escapeshellarg($visitor['visitor_id']);
62+
63+
// string type attribute values
64+
$attributes = $visitor['attributes'];
65+
$escapedAttributes = [];
66+
67+
foreach ($attributes as $attr) {
68+
$attr['value'] = $this->escapeIfString($attr['value']);
69+
array_push($escapedAttributes, $attr);
70+
}
71+
72+
$visitor['attributes'] = $escapedAttributes;
73+
74+
// event tags if present
75+
$escapedEventTags = [];
76+
77+
if (isset($visitor['snapshots'][0]['events'][0]['tags'])) {
78+
$eventTags = $visitor['snapshots'][0]['events'][0]['tags'];
79+
80+
foreach ($eventTags as $key => $value) {
81+
$key = $this->escapeIfString($key);
82+
$value = $this->escapeIfString($value);
83+
84+
$escapedEventTags[$key] = $value;
85+
}
86+
}
87+
88+
$visitor['snapshots'][0]['events'][0]['tags'] = $escapedEventTags;
89+
}
90+
91+
return $params;
92+
}
93+
3094
public function dispatchEvent(LogEvent $event)
3195
{
3296
$cmd = "curl";
3397
$cmd.= " -X ".$event->getHttpVerb();
3498
foreach ($event->getHeaders() as $type => $value) {
3599
$cmd.= " -H '".$type.": ".$value."'";
36100
}
37-
$cmd.= " -d '".json_encode($event->getParams())."'";
101+
102+
$eventParams = $this->sanitizeEventPayload($event->getParams());
103+
104+
$cmd.= " -d '".json_encode($eventParams)."'";
38105
$cmd.= " '".$event->getUrl()."' > /dev/null 2>&1 &";
39106
exec($cmd, $output, $exit_code);
40107

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
/**
3+
* Copyright 2019, Optimizely
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
namespace Optimizely\Tests;
18+
19+
use Optimizely\Event\Builder\EventBuilder;
20+
use Optimizely\Event\Dispatcher\CurlEventDispatcher;
21+
use Optimizely\Event\LogEvent;
22+
use Optimizely\ErrorHandler\NoOpErrorHandler;
23+
use Optimizely\Logger\NoOpLogger;
24+
use Optimizely\ProjectConfig;
25+
26+
class CurlEventDispatcherTest extends \PHPUnit_Framework_TestCase
27+
{
28+
public function setUp()
29+
{
30+
$logger = new NoOpLogger();
31+
$this->config = new ProjectConfig(DATAFILE, $logger, new NoOpErrorHandler());
32+
$this->eventBuilder = new EventBuilder($logger);
33+
}
34+
35+
// Test that user input values are escaped. This includes
36+
// 1. user ID.
37+
// 2. attribute values of type string.
38+
// 3. event tag keys.
39+
// 4. event tag values of type string.
40+
public function testSanitizeEventPayloadWithEventTags()
41+
{
42+
$eventDispatcher = new CurlEventDispatcher();
43+
44+
$userAttributes = [
45+
'device_type' => 'iPhone',
46+
'company' => 'Optimizely',
47+
];
48+
49+
$logEvent = $this->eventBuilder->createConversionEvent(
50+
$this->config,
51+
'purchase',
52+
'testUserId',
53+
$userAttributes,
54+
array(
55+
'revenue' => 56,
56+
'value'=> '13.37',
57+
'boolean-tag' => false,
58+
'float' => 5.5,
59+
'integer' => 6
60+
)
61+
);
62+
63+
$expectedParams = $logEvent->getParams();
64+
65+
$expectedUserId = escapeshellarg('testUserId');
66+
$expectedParams['visitors'][0]['visitor_id'] = $expectedUserId;
67+
68+
$expectedAttributes = $expectedParams['visitors'][0]['attributes'];
69+
foreach ($expectedAttributes as &$attr) {
70+
if (in_array($attr['key'], ['device_type', 'company'])) {
71+
$attr['value'] = escapeshellarg($attr['value']);
72+
}
73+
}
74+
75+
$expectedParams['visitors'][0]['attributes'] = $expectedAttributes;
76+
77+
$expectedEventTags = [];
78+
$expectedEventTags[escapeshellarg('revenue')] = 56;
79+
$expectedEventTags[escapeshellarg('value')] = escapeshellarg('13.37');
80+
$expectedEventTags[escapeshellarg('boolean-tag')] = false;
81+
$expectedEventTags[escapeshellarg('float')] = 5.5;
82+
$expectedEventTags[escapeshellarg('integer')] = 6;
83+
84+
$expectedParams['visitors'][0]['snapshots'][0]['events'][0]['tags'] = $expectedEventTags;
85+
86+
$this->assertEquals($expectedParams, $eventDispatcher->sanitizeEventPayload($logEvent->getParams()));
87+
}
88+
}

0 commit comments

Comments
 (0)