├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json └── src └── EnforceContentSecurity.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #Changelog 2 | 3 | All Notable changes to `laravel-middleware-csp` will be documented in this file 4 | 5 | ## 0.1.1 - 2015-08-06 6 | 7 | ### Added 8 | - Migrate to new namespace 9 | 10 | ### Deprecated 11 | - Nothing 12 | 13 | ### Fixed 14 | - Nothing 15 | 16 | ### Removed 17 | - Nothing 18 | 19 | ### Security 20 | - Nothing 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/stevenmaguire/laravel-middleware-csp). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ phpunit 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Steven Maguire 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Content Security Policy Middleware 2 | 3 | [![Latest Version](https://img.shields.io/github/release/stevenmaguire/laravel-middleware-csp.svg?style=flat-square)](https://github.com/stevenmaguire/laravel-middleware-csp/releases) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 5 | [![Build Status](https://img.shields.io/travis/stevenmaguire/laravel-middleware-csp/master.svg?style=flat-square)](https://travis-ci.org/stevenmaguire/laravel-middleware-csp) 6 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/stevenmaguire/laravel-middleware-csp.svg?style=flat-square)](https://scrutinizer-ci.com/g/stevenmaguire/laravel-middleware-csp/code-structure) 7 | [![Quality Score](https://img.shields.io/scrutinizer/g/stevenmaguire/laravel-middleware-csp.svg?style=flat-square)](https://scrutinizer-ci.com/g/stevenmaguire/laravel-middleware-csp) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/stevenmaguire/laravel-middleware-csp.svg?style=flat-square)](https://packagist.org/packages/stevenmaguire/laravel-middleware-csp) 9 | 10 | Provides support for enforcing Content Security Policy with headers in Laravel responses. This package extends and utilizes the [framework agnostic Content Security Policy Middleware for PSR 7 response](https://github.com/stevenmaguire/middleware-csp-php). 11 | 12 | ## Install 13 | 14 | Via Composer 15 | 16 | ``` bash 17 | $ composer require stevenmaguire/laravel-middleware-csp 18 | ``` 19 | 20 | ## Usage 21 | 22 | ### Register as route middleware 23 | 24 | ``` php 25 | // within app/Http/Kernal.php 26 | 27 | protected $routeMiddleware = [ 28 | // 29 | 'secure.content' => \Stevenmaguire\Laravel\Http\Middleware\EnforceContentSecurity::class, 30 | // 31 | ]; 32 | ``` 33 | 34 | ### Apply content security policy to routes 35 | 36 | The following will apply all default profiles to the `gallery` route. 37 | 38 | ``` php 39 | // within app/Http/routes.php 40 | 41 | Route::get('gallery', ['middleware' => 'secure.content'], function () { 42 | return 'pictures!'; 43 | }); 44 | ``` 45 | 46 | The following will apply all default profiles and a specific `flickr` profile to the `gallery` route. 47 | 48 | ``` php 49 | // within app/Http/routes.php 50 | 51 | Route::get('gallery', ['middleware' => 'secure.content:flickr'], function () { 52 | return 'pictures!'; 53 | }); 54 | ``` 55 | 56 | 57 | ### Apply content security policy to controllers 58 | 59 | The following will apply all default profiles to all methods within the `GalleryController`. 60 | 61 | ``` php 62 | // within app/Http/Controllers/GalleryController.php 63 | 64 | public function __construct() 65 | { 66 | $this->middleware('secure.content'); 67 | } 68 | ``` 69 | The following will apply all default profiles and a specific `google` profile to all methods within the `GalleryController`. 70 | 71 | ``` php 72 | // within app/Http/Controllers/GalleryController.php 73 | 74 | public function __construct() 75 | { 76 | $this->middleware('secure.content:google'); 77 | } 78 | ``` 79 | You can include any number of specific profiles to any middleware decoration. For instance, the following will apply default, `google`, `flickr`, and `my_custom` profiles to all methods within the `GalleryController`. 80 | 81 | ``` php 82 | // within app/Http/Controllers/GalleryController.php 83 | 84 | public function __construct() 85 | { 86 | $this->middleware('secure.content:google,flickr,my_custom'); 87 | } 88 | ``` 89 | 90 | ### Create content security profiles 91 | 92 | The default location for content security profiles is `security.content`. If you wish to use this default configuration, ensure your project includes the appropriate configuration files. 93 | 94 | You can find all available options on the owasp [CSP Cheat Sheet](https://www.owasp.org/index.php/Content_Security_Policy_Cheat_Sheet). 95 | 96 | The structure of this configuration array is important. The middleware expects to find a `default` key with a string value and a `profiles` key with an array value. 97 | 98 | ``` php 99 | // within config/security.php 100 | 101 | return [ 102 | 'content' => [ 103 | 'default' => '', 104 | 'profiles' => [], 105 | ], 106 | ]; 107 | 108 | ``` 109 | The `profiles` array contains the security profiles for your application. Each profile name must be unique and is expected to have a value of an array. 110 | 111 | ``` php 112 | // within config/security.php 113 | 114 | return [ 115 | 'content' => [ 116 | 'default' => '', 117 | 'profiles' => [ 118 | 'profile_one' => [], 119 | 'profile_two' => [], 120 | 'profile_three' => [], 121 | ], 122 | ], 123 | ]; 124 | 125 | ``` 126 | Each profile array should contain keys that correspond to Content Security Policy directives. The value of each of these directives can be a string, comma-separated string, or array of strings. Each string value should correspond to the domain associated with your directive and profile. 127 | 128 | ``` php 129 | // within config/security.php 130 | 131 | return [ 132 | 'content' => [ 133 | 'default' => '', 134 | 'profiles' => [ 135 | 'profile_one' => [ 136 | 'base-uri' => 'https://domain.com,http://google.com', 137 | ], 138 | 'profile_two' => [ 139 | 'font-src' => 'https://domain.com', 140 | 'base-uri' => [ 141 | "'self'", 142 | 'http://google.com' 143 | ], 144 | ], 145 | 'profile_three' => [ 146 | 'font-src' => [ 147 | "'self'" 148 | ], 149 | ], 150 | ], 151 | ], 152 | ]; 153 | 154 | ``` 155 | The `default` key value should be a string, comma-separated string, or array of strings that correspond to the unique profile names that you would like to enforce on all responses with minimal content security applied. 156 | 157 | ``` php 158 | // within config/security.php 159 | 160 | return [ 161 | 'content' => [ 162 | 'default' => 'profile_one', 163 | 'profiles' => [ 164 | 'profile_one' => [ 165 | 'base-uri' => 'https://domain.com,http://google.com', 166 | ], 167 | 'profile_two' => [ 168 | 'font-src' => 'https://domain.com', 169 | 'base-uri' => [ 170 | "'self'", 171 | 'http://google.com' 172 | ], 173 | ], 174 | 'profile_three' => [ 175 | 'font-src' => [ 176 | "'self'" 177 | ], 178 | ], 179 | ], 180 | ], 181 | ]; 182 | 183 | ``` 184 | 185 | Here is a real-world example: 186 | 187 | ``` php 188 | // within config/security.php 189 | 190 | return [ 191 | 'content' => [ 192 | 'default' => 'global', 193 | 'profiles' => [ 194 | 'global' => [ 195 | 'base-uri' => "'self'", 196 | 'default-src' => "'self'", 197 | 'font-src' => [ 198 | "'self'", 199 | 'fonts.gstatic.com' 200 | ], 201 | 'img-src' => "'self'", 202 | 'script-src' => "'self'", 203 | 'style-src' => [ 204 | "'self'", 205 | "'unsafe-inline'", 206 | 'fonts.googleapis.com' 207 | ], 208 | ], 209 | 'flickr' => [ 210 | 'img-src' => [ 211 | 'https://*.staticflickr.com', 212 | ], 213 | ], 214 | ], 215 | ], 216 | ]; 217 | 218 | ``` 219 | 220 | ## Testing 221 | 222 | ``` bash 223 | $ ./vendor/bin/phpunit 224 | ``` 225 | 226 | ## Contributing 227 | 228 | Please see [CONTRIBUTING](https://github.com/stevenmaguire/laravel-middleware-csp/blob/master/CONTRIBUTING.md) for details. 229 | 230 | ## Credits 231 | 232 | - [Steven Maguire](https://github.com/stevenmaguire) 233 | - [All Contributors](https://github.com/stevenmaguire/laravel-middleware-csp/contributors) 234 | 235 | ## License 236 | 237 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 238 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stevenmaguire/laravel-middleware-csp", 3 | "description": "Provides support for enforcing Content Security Policy with headers in Laravel responses.", 4 | "keywords": [ 5 | "middleware", 6 | "psr7", 7 | "content security policy", 8 | "headers", 9 | "laravel" 10 | ], 11 | "homepage": "https://github.com/stevenmaguire/laravel-middleware-csp", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Steven Maguire", 16 | "email": "stevenmaguire@gmail.com", 17 | "homepage": "https://github.com/stevenmaguire", 18 | "role": "Developer" 19 | } 20 | ], 21 | "require": { 22 | "php" : ">=5.5.9", 23 | "stevenmaguire/middleware-csp": "^0.1", 24 | "guzzlehttp/psr7": "^1.1", 25 | "illuminate/http": "^5.1" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "3.7.*", 29 | "mockery/mockery": "0.9.*@dev", 30 | "squizlabs/php_codesniffer": "~2.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Stevenmaguire\\Laravel\\Http\\Middleware\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Stevenmaguire\\Laravel\\Http\\Middleware\\Test\\": "tests" 40 | } 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-master": "1.0-dev" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/EnforceContentSecurity.php: -------------------------------------------------------------------------------- 1 | setConfigClosure(function ($key = null, $default = null) { 24 | // @codeCoverageIgnoreStart 25 | if (function_exists('config')) { 26 | return config($key, $default); 27 | } 28 | 29 | return null; 30 | // @codeCoverageIgnoreEnd 31 | }); 32 | } 33 | 34 | /** 35 | * Handles an incoming request. 36 | * 37 | * @param \Illuminate\Http\Request $request 38 | * @param \Closure $next 39 | * @return mixed 40 | */ 41 | public function handle($request, Closure $next) 42 | { 43 | $response = $next($request); 44 | 45 | if ($response instanceof Response) { 46 | $this->setProfiles($this->getProfileConfig()); 47 | 48 | $this->setProfilesWithParameters(func_get_args()); 49 | 50 | $psr7Response = $this->createPsr7Response($response); 51 | 52 | $psr7Response = $this->addPolicyHeader($psr7Response); 53 | 54 | $response = $this->createLaravelResponse($psr7Response); 55 | } 56 | 57 | return $response; 58 | } 59 | 60 | /** 61 | * Creates Laravel response object from PSR 7 response. 62 | * 63 | * @param ResponseInterface $response 64 | * 65 | * @return Response 66 | */ 67 | protected function createLaravelResponse(ResponseInterface $response) 68 | { 69 | return new Response( 70 | (string) $response->getBody(), 71 | $response->getStatusCode(), 72 | $response->getHeaders() 73 | ); 74 | } 75 | 76 | /** 77 | * Creates PSR 7 response object from Laravel response. 78 | * 79 | * @param Response $response 80 | * 81 | * @return ResponseInterface 82 | */ 83 | protected function createPsr7Response(Response $response) 84 | { 85 | return new PsrResponse( 86 | $response->getStatusCode(), 87 | $response->headers->all(), 88 | $response->getContent(), 89 | $response->getProtocolVersion() 90 | ); 91 | } 92 | 93 | /** 94 | * Retrives profile configuration from Laravel config object. 95 | * 96 | * @return array 97 | */ 98 | protected function getProfileConfig() 99 | { 100 | $configCallable = $this->config; 101 | $config = $configCallable($this->getProfileConfigKey()); 102 | 103 | if (!is_array($config)) { 104 | $config = [$config]; 105 | } 106 | 107 | return array_filter($config); 108 | } 109 | 110 | /** 111 | * Retrieves configuration key associated with content security profiles. 112 | * 113 | * @return string 114 | */ 115 | protected function getProfileConfigKey() 116 | { 117 | return 'security.content'; 118 | } 119 | 120 | /** 121 | * Gets profiles from handle method arguments. 122 | * 123 | * @param array $arguments 124 | * 125 | * @return array 126 | */ 127 | protected function getProfilesFromArguments(array $arguments) 128 | { 129 | $profiles = []; 130 | if (count($arguments) > 2) { 131 | unset($arguments[0]); 132 | unset($arguments[1]); 133 | $profiles = $arguments; 134 | } 135 | return $profiles; 136 | } 137 | 138 | /** 139 | * Updates config callable used to access application configuration data. 140 | * 141 | * @param Closure $config 142 | * 143 | * @return EnforceContentSecurity 144 | */ 145 | public function setConfigClosure(Closure $config) 146 | { 147 | $this->config = $config; 148 | 149 | return $this; 150 | } 151 | 152 | /** 153 | * Updates policy configuration with rules from each profile in given parameters. 154 | * 155 | * @param array $parameters 156 | * 157 | * @return void 158 | */ 159 | protected function setProfilesWithParameters(array $parameters) 160 | { 161 | $profiles = $this->getProfilesFromArguments($parameters); 162 | array_map([$this, 'loadProfileByKey'], $profiles); 163 | } 164 | } 165 | --------------------------------------------------------------------------------