From 59f253be46ef13877200b24f6fe8db5c1fe342fd Mon Sep 17 00:00:00 2001 From: Seiji Naganuma Date: Fri, 10 Apr 2020 16:28:37 -1000 Subject: [PATCH] [Feature] Add ability to sanitize post fields (#13) --- README.md | 45 +++++++++++++++++++++ src/Config.php | 6 +++ src/VCRCleaner.php | 9 +++-- src/VCRCleanerEventSubscriber.php | 16 +++++++- tests/VCR/VCRCleanerEventSubscriberTest.php | 32 +++++++++++++++ 5 files changed, 102 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3009f86..ed90ff8 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,12 @@ VCRCleaner::enable(array( return preg_replace('//', 'hunter2', $body); } ), + 'postFieldScrubbers' => array( + function(array $postFields) { + $postFields['Secret'] = 'REDACTED'; + return $postFields; + } + ), ), 'response' => array( 'ignoreHeaders' => array(), @@ -74,6 +80,7 @@ This library allows your sanitize both the Request and Response sections of your - `request.ignoreQueryFields` - Define which GET parameters in your URL to completely strip out of your recordings. - `request.ignoreHeaders` - Define the headers in your recording that will automatically be set to null in your recordings - `request.bodyScrubbers` - An array of callbacks that will have the request body available as a string. Each callback **must** return the modified body. The callbacks are called consecutively in the order they appear in this array and the value from one callback propagates to the next. +- `request.postFieldScrubbers` - An array of callbacks that will have the request post fields available as an array. Each callback **must** return the modified post fields array. The callbacks are called consecutively in the order they appear in this array and the value from one callback propagates to the next. #### Sanitizing Responses @@ -197,6 +204,44 @@ VCRCleaner::enable(array( body: '...response body...' ``` +### Post Field Content + +When making POST requests, your VCR will sometimes record the data inside of a `post_fields` parameter rather than the `body`; e.g. when `CURLOPT_POSTFIELDS` is used in cURL and you do not set `CURLOPT_POST` to `true`. In those cases, this option can be used to sanitize sensitive content. Note that unlike the `body` field, `post_fields` is an array: + +```php +VCRCleaner::enable(array( + 'request' => array( + 'postFieldScrubber' => array( + function (array $postFields) { + $postFields['Secret_Key'] = ''; + return $postFields; + }, + ), + ), +)); +``` + +```yaml +# You POST request to `https://www.example.com/search` with a post field of +# `['data'=> 'hello world', 'Secret_Key' => 'abc']` gets recorded like so, +- + request: + method: POST + url: 'https://www.example.com/search' + headers: + Host: www.example.com + post_fields: + data: 'hello world' + Secret_Key: '' + response: + status: + http_version: '1.1' + code: '404' + message: 'Not Found' + headers: ~ + body: '...response body...' +``` + ## License [MIT](/LICENSE.md) diff --git a/src/Config.php b/src/Config.php index 5c449c7..9a263e6 100644 --- a/src/Config.php +++ b/src/Config.php @@ -41,6 +41,11 @@ public static function getReqBodyScrubbers() return self::$options['request']['bodyScrubbers']; } + public static function getReqPostFieldScrubbers() + { + return self::$options['request']['postFieldScrubbers']; + } + public static function getResIgnoredHeaders() { return self::$options['response']['ignoreHeaders']; @@ -59,6 +64,7 @@ private static function defaultConfig() 'ignoreQueryFields' => array(), 'ignoreHeaders' => array(), 'bodyScrubbers' => array(), + 'postFieldScrubbers'=> array(), ), 'response' => array( 'ignoreHeaders' => array(), diff --git a/src/VCRCleaner.php b/src/VCRCleaner.php index 9d81752..c1faf9e 100644 --- a/src/VCRCleaner.php +++ b/src/VCRCleaner.php @@ -20,10 +20,11 @@ abstract class VCRCleaner * ``` * $options = [ * 'request' => [ - * 'ignoreHostname' => boolean - * 'ignoreQueryFields' => string[] - * 'ignoreHeaders' => string[] - * 'bodyScrubbers' => Array<(string $body): string> + * 'ignoreHostname' => boolean + * 'ignoreQueryFields' => string[] + * 'ignoreHeaders' => string[] + * 'bodyScrubbers' => Array<(string $body): string> + * 'postFieldScrubbers' => Array<(array $postFields): array> * ], * 'response' => [ * 'ignoreHeaders' => string[] diff --git a/src/VCRCleanerEventSubscriber.php b/src/VCRCleanerEventSubscriber.php index 36fe2af..28267d6 100644 --- a/src/VCRCleanerEventSubscriber.php +++ b/src/VCRCleanerEventSubscriber.php @@ -45,6 +45,7 @@ public function onBeforeRecord(BeforeRecordEvent $event) $this->sanitizeRequestUrl($event->getRequest()); $this->sanitizeRequestHeaders($event->getRequest()); $this->sanitizeRequestBody($event->getRequest()); + $this->sanitizeRequestPostFields($event->getRequest()); $originalRes = $event->getResponse(); @@ -71,7 +72,7 @@ public function onBeforeRecord(BeforeRecordEvent $event) private function sanitizeRequestHeaders(Request $request) { - $caseInsensitiveKeys = []; + $caseInsensitiveKeys = array(); foreach ($request->getHeaders() as $key => $value) { $caseInsensitiveKeys[strtolower($key)] = $key; @@ -139,11 +140,22 @@ private function sanitizeRequestBody(Request $request) $request->setBody($body); } + private function sanitizeRequestPostFields(Request $request) + { + $postFields = $request->getPostFields(); + + foreach (Config::getReqPostFieldScrubbers() as $scrubber) { + $postFields = $scrubber($postFields); + } + + $request->setPostFields($postFields); + } + private function sanitizeResponseHeaders(array &$workspace) { // To avoid breaking case-sensitivity in cassettes, keep a record of the // mapping between lowercase to original casing. - $caseInsensitiveKeys = []; + $caseInsensitiveKeys = array(); foreach ($workspace['headers'] as $key => $value) { $caseInsensitiveKeys[strtolower($key)] = $key; diff --git a/tests/VCR/VCRCleanerEventSubscriberTest.php b/tests/VCR/VCRCleanerEventSubscriberTest.php index 1091c2a..8ceab2e 100644 --- a/tests/VCR/VCRCleanerEventSubscriberTest.php +++ b/tests/VCR/VCRCleanerEventSubscriberTest.php @@ -183,6 +183,38 @@ public function testCurlCallWithSensitiveBody() $this->assertContains('REDACTED', $vcrFile); } + public function testCurlCallWithSensitivePostField() + { + $cb = function (array $postFields) { + $postFields['VerySecret'] = 'REDACTED'; + + return $postFields; + }; + + VCRCleaner::enable(array( + 'request' => array( + 'postFieldScrubbers' => array( + $cb, + ), + ), + )); + + $curl = new Curl(); + $secret = 'Do not tell anyone this secret'; + $postFields = array( + 'SomethingPublic' => 'Not a secret', + 'VerySecret' => $secret, + ); + $curl->setOpt(CURLOPT_POSTFIELDS, $postFields); + $curl->post('https://www.example.com/search'); + $curl->close(); + + $vcrFile = $this->getCassetteContent(); + + $this->assertNotContains($secret, $vcrFile); + $this->assertContains('REDACTED', $vcrFile); + } + public function testCurlCallWithRedactedHostname() { VCRCleaner::enable(array(