.gitignore 0000666 00000000025 13244772066 0006550 0 ustar 00 composer.lock
vendor
.travis.yml 0000666 00000000704 13244772066 0006675 0 ustar 00 language: php
cache:
directories:
- .phpunit
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
- nightly
before_install:
- phpenv config-rm xdebug.ini || true
- composer self-update
install:
- if [[ $TRAVIS_PHP_VERSION != '5.5' ]]; then composer install ; fi
- if [[ $TRAVIS_PHP_VERSION == '5.5' ]]; then composer update --prefer-lowest ; fi
script:
- export SYMFONY_PHPUNIT_DIR=`pwd`/.phpunit
- ./vendor/bin/simple-phpunit
CHANGELOG.md 0000666 00000012066 13244772066 0006401 0 ustar 00 ### 2.5.0 (2017-xx-xx)
* Allows matching the query parameter for clickjacking protection
* Cleanup content type restrictable listener
* Added Symfony 4 support
* Added support for 'worker-src' CSP directive
* Removed PHP 5.3 support guarantees
* Fix CSP noise filter compiler pass registration
### 2.4.0 (2017-06-22)
* Deprecate calling ContentSecurityPolicyListener::getNonce without usage ('script' or 'style')
* Added `forced_ssl > redirect_status_code` option to allow switching to permanent redirect (301) responses
* Fixed HSTS header being sent even in non-secure responses unnecessarily
* Fixed URLs with whitespace prefix not being seen as external redirects
### 2.3.1 (2017-03-17)
* Fix arguments for Twig extension
### 2.3.0 (2017-03-17)
* Add support for script-src 'strict-dynamic' (see https://w3c.github.io/webappsec-csp/#strict-dynamic-usage)
* Improve CSP filtering
* Remove Twig extension compiler pass in favor of tag
* Use symfony/phpunit-bridge for testing on IC
### 2.2.4 (2017-02-13)
* Fix exceptions thrown by Report::fromRequest
### 2.2.3 (2017-02-13)
* Improve CSP filtering
### 2.2.2 (2017-02-07)
* Improve CSP filtering
* Fix injected script noise detector loading
### 2.2.1 (2017-02-07)
* Fix dependency on UAParser
### 2.2.0 (2017-02-06)
* Add CSP report filter
* Fix Twig 2 support
### 2.1.0 (2017-01-26)
* Add support for Referrer Policy
* Content-Security-Policy header can now be disabled
* Fix encrypter deprecation
* Run the test suite on PHP 7.1
* Run the test suite with lowest dependencies
### 2.0.4 (2016-10-19)
* Enable manifest-src directive for Chrome, Opera and Firefox
### 2.0.3 (2016-10-13)
* Fix deprecation warning with latest Twig 1.x
### 2.0.2 (2016-08-24)
* Fix typo in the ALLOW-FROM implementation
* Update browser_adaptive configuration. Allow custom adapters
* Add Doctrine Cache and Psr Cache adapters for caching UA family parser
### 2.0.1 (2016-06-04)
* Fix CookieSessionHandler::open that should return true unless there's an error
### 2.0.0 (2016-05-17)
* Add support for Content-Security-Policy Level 2 directives
* Add support for Content-Security-Policy Level 2 signatures (nonce and message digest)
* Add browser adaptive directives - do not send directives not supported by browser - via browser_adaptive parameter
* Allow report-uri to be defined as a scalar
* Deprecate encrypted cookie support du to high coupling to mcrypt deprecated extension
* Drop backward-compatibility with first deprecated CSP configuration
### 1.10.0 (2016-02-23)
* Added ability to restrict forced_ssl capability to some hostnames only
* Fixed Symfony 3 compatibility
### 1.9.1 (2016-01-17)
* BugFix: Fix LoggerInterface type hints to support PSR-3 loggers and not only Symfony 2.0 loggers
### 1.9.0 (2016-01-04)
* Add Symfony 3 compatibility
* external_redirects definition can now contains full URL
* Allow dynamic CSP configuration
* BugFix: Fix clickjacking URL normalization when containing dash and no underscore
### 1.8.0 (2015-09-12)
* Added HTTP response's content-type restriction for Clickjacking and CSP headers.
* Added Microsoft's XSS-Protection support
* Disabled Clickjacking, CSP and NoSniff headers in the context of HTTP redirects
* Fixed bug in handling of the external_redirects.log being disabled
### 1.7.0 (2015-05-10)
* Added a `Nelmio\SecurityBundle\ExternalRedirect\TargetValidator` interface to implement custom rules for the external_redirects feature. You can override the `nelmio_security.external_redirect.target_validator` service to change the default.
* Added a `hosts` key in the CSP configuration to restrict CSP-checks to some host names
* Fixed a bug in `flexible_ssl` where the auth cookie was updated with a wrong expiration time the second time the visitor comes to the site.
* Removed X-Webkit-CSP header as none of the webkits using it are still current.
### 1.6.0 (2015-02-01)
* Added a `forced_ssl.hsts_preload` flag to allow adding the preload attribute on HSTS headers
### 1.5.0 (2015-01-01)
* Added ability to have different configs for both reported and enforced CSP rules
* Added support for ALLOW and ALLOW-FROM syntaxes in the Clickjacking Protection
* Added support for HHVM and PHP 5.6
* Fixed enabling of cookie signing when the cookie list is empty
### 1.4.0 (2014-02-13)
* Added default controller to log CSP violations
* Added a flag to remove outdated non-standard CSP headers and only send the `Content-Security-Policy` one
### 1.3.0 (2014-01-08)
* Added support for setting the X-Content-Type-Options header
### 1.2.0 (2013-07-29)
* Added Content-Security-Policy (CSP) 1.0 support
* Added forced_ssl.whitelist property to define URLs that do not need to be force-redirected
* Fixed session loss bug on 404 URLs in the CookieSessionHandler
### 1.1.0 (2013-03-27)
* Added a cookie session storage (use only if really needed, and combine it with `encrypted_cookie`)
* Fixed error reporting if mcrypt is not enabled and you try to use encryption
### 1.0.0 (2013-01-08)
* Initial release
ContentSecurityPolicy/ContentSecurityPolicyParser.php 0000666 00000002274 13244772066 0017322 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\ContentSecurityPolicy;
class ContentSecurityPolicyParser
{
protected $keywords = array('self', 'none', 'unsafe-inline', 'unsafe-eval', 'strict-dynamic');
/**
* @param array $sourceList
*
* @return string
*/
public function parseSourceList($sourceList)
{
if (!is_array($sourceList)) {
return $sourceList;
}
$sourceList = $this->quoteKeywords($sourceList);
return implode(' ', $sourceList);
}
/**
* @param array $sourceList
*
* @return array
*/
protected function quoteKeywords(array $sourceList)
{
$keywords = $this->keywords;
return array_map(
function ($source) use ($keywords) {
if (in_array($source, $keywords, true)) {
return sprintf("'%s'", $source);
}
return $source;
},
$sourceList
);
}
}
ContentSecurityPolicy/DirectiveSet.php 0000666 00000015305 13244772066 0014214 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\ContentSecurityPolicy;
use Symfony\Component\HttpFoundation\Request;
class DirectiveSet
{
const TYPE_SRC_LIST = 'source-list';
const TYPE_SRC_LIST_NOFB = 'source-list-no-fallback';
const TYPE_MEDIA_TYPE_LIST = 'media-type-list';
const TYPE_ANCESTOR_SRC_LIST = 'ancestor-source-list';
const TYPE_URI_REFERENCE = 'uri-reference';
const TYPE_NO_VALUE = 'no-value';
private static $directiveNames = array(
'default-src' => self::TYPE_SRC_LIST,
'base-uri' => self::TYPE_SRC_LIST_NOFB,
'block-all-mixed-content' => self::TYPE_NO_VALUE,
'child-src' => self::TYPE_SRC_LIST,
'connect-src' => self::TYPE_SRC_LIST,
'font-src' => self::TYPE_SRC_LIST,
'form-action' => self::TYPE_SRC_LIST_NOFB,
'frame-ancestors' => self::TYPE_ANCESTOR_SRC_LIST,
'frame-src' => self::TYPE_SRC_LIST,
'img-src' => self::TYPE_SRC_LIST,
'manifest-src' => self::TYPE_SRC_LIST,
'media-src' => self::TYPE_SRC_LIST,
'object-src' => self::TYPE_SRC_LIST,
'plugin-types' => self::TYPE_MEDIA_TYPE_LIST,
'script-src' => self::TYPE_SRC_LIST,
'style-src' => self::TYPE_SRC_LIST,
'upgrade-insecure-requests' => self::TYPE_NO_VALUE,
'report-uri' => self::TYPE_URI_REFERENCE,
'worker-src' => self::TYPE_SRC_LIST,
);
private $directiveValues = array();
private $level1Fallback = true;
private $policyManager = null;
public function __construct(PolicyManager $policyManager)
{
$this->policyManager = $policyManager;
}
public function setLevel1Fallback($bool)
{
$this->level1Fallback = (bool) $bool;
}
public function getDirective($name)
{
$this->checkDirectiveName($name);
if (array_key_exists($name, $this->directiveValues)) {
return $this->directiveValues[$name];
}
return '';
}
public function setDirective($name, $value)
{
$this->checkDirectiveName($name);
if (self::$directiveNames[$name] === self::TYPE_NO_VALUE) {
if ($value) {
$this->directiveValues[$name] = true;
} else {
unset($this->directiveValues[$name]);
}
} elseif ($value) {
$this->directiveValues[$name] = $value;
} else {
unset($this->directiveValues[$name]);
}
}
public function setDirectives(array $directives)
{
foreach ($directives as $name => $value) {
$this->setDirective($name, $value);
}
}
public function buildHeaderValue(Request $request, array $signatures = null)
{
$policy = array();
if (isset($signatures['script-src'])) {
$signatures['script-src'] = implode(' ', array_map(function ($value) { return sprintf('\'%s\'', $value); }, $signatures['script-src']));
}
if (isset($signatures['style-src'])) {
$signatures['style-src'] = implode(' ', array_map(function ($value) { return sprintf('\'%s\'', $value); }, $signatures['style-src']));
}
$availableDirectives = $this->policyManager->getAvailableDirective($request);
foreach ($this->directiveValues as $name => $value) {
if (!in_array($name, $availableDirectives, true)) {
continue;
}
if (true === $value) {
$policy[] = $name;
} elseif (isset($signatures[$name])) {
// since a hash / nonce is used (CSP level2)
// In case the browsers support CSP level 2, it would discard the 'unsafe-inline' directive
// let's ensure that it's backward compatible with CSP level 1 (all browsers are not compatible)
// this is the recommended way to deal with this.
if (false === strpos($value, '\'unsafe-inline\'') && $this->level1Fallback) {
$policy[] = $name.' '.$value.' '.'\'unsafe-inline\' '.$signatures[$name];
} else {
$policy[] = $name.' '.$value.' '.$signatures[$name];
}
} elseif ($this->canNotBeFallbackedByDefault($name, $value)) {
$policy[] = $name.' '.$value;
}
}
if (!empty($signatures)) {
$defaultSrc = $this->getDirective('default-src');
$isDefaultSrcSet = $defaultSrc !== '';
if ($isDefaultSrcSet && false === strpos($defaultSrc, '\'unsafe-inline\'')) {
$unsafeInline = $this->level1Fallback ? ' \'unsafe-inline\'' : '';
if (empty($this->directiveValues['script-src']) && isset($signatures['script-src'])) {
$policy[] = 'script-src '.$defaultSrc.$unsafeInline.' '.$signatures['script-src'];
}
if (empty($this->directiveValues['style-src']) && isset($signatures['style-src'])) {
$policy[] = 'style-src '.$defaultSrc.$unsafeInline.' '.$signatures['style-src'];
}
}
}
return implode('; ', $policy);
}
public static function fromConfig(PolicyManager $policyManager, array $config, $kind)
{
$directiveSet = new self($policyManager);
$directiveSet->setLevel1Fallback(isset($config[$kind]) ? $config[$kind]['level1_fallback'] : false);
if (!array_key_exists($kind, $config)) {
return $directiveSet;
}
$parser = new ContentSecurityPolicyParser();
foreach (self::getNames() as $name => $type) {
if (!array_key_exists($name, $config[$kind])) {
continue;
}
$directiveSet->setDirective($name, $parser->parseSourceList($config[$kind][$name]));
}
return $directiveSet;
}
public static function getNames()
{
return self::$directiveNames;
}
private function checkDirectiveName($name)
{
if (!array_key_exists($name, self::$directiveNames)) {
throw new \InvalidArgumentException('Unknown CSP directive name: '.$name);
}
}
private function canNotBeFallbackedByDefault($name, $value)
{
if ($name === 'default-src') {
return true;
}
// Only source-list can be fallbacked by default
if (self::$directiveNames[$name] !== self::TYPE_SRC_LIST) {
return true;
}
// let's fallback if directives are strictly equals
return $value !== $this->getDirective('default-src');
}
}
ContentSecurityPolicy/NonceGenerator.php 0000666 00000001303 13244772066 0014524 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\ContentSecurityPolicy;
class NonceGenerator
{
/**
* @var int
*/
private $numberOfBytes;
public function __construct($numberOfBytes)
{
$this->numberOfBytes = $numberOfBytes;
}
/**
* Generates a nonce value that is later used in script and style policies.
*
* @return string
*/
public function generate()
{
return bin2hex(random_bytes($this->numberOfBytes));
}
}
ContentSecurityPolicy/PolicyManager.php 0000666 00000006067 13244772066 0014361 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\ContentSecurityPolicy;
use Nelmio\SecurityBundle\UserAgent\UserAgentParserInterface;
use Symfony\Component\HttpFoundation\Request;
class PolicyManager
{
private $uaParser;
public function __construct(UserAgentParserInterface $uaParser = null)
{
$this->uaParser = $uaParser;
}
/**
* Returns the list of supported directives for the current Request
*
* @param Request $request
*
* @return array
*/
public function getAvailableDirective(Request $request)
{
if (null === $this->uaParser) {
return $this->getChromeDirectives();
}
switch ($this->uaParser->getBrowser($request->headers->get('user-agent'))) {
case UserAgentParserInterface::BROWSER_CHROME:
case UserAgentParserInterface::BROWSER_OPERA:
case UserAgentParserInterface::BROWSER_OTHER:
return $this->getChromeDirectives();
case UserAgentParserInterface::BROWSER_FIREFOX:
return $this->getFirefoxDirectives();
case UserAgentParserInterface::BROWSER_SAFARI:
return $this->getLevel1();
}
}
private function getChromeDirectives()
{
return array_merge($this->getLevel3(), $this->getDraftDirectives());
}
private function getFirefoxDirectives()
{
return array_diff(array_merge($this->getLevel3(), $this->getDraftDirectives()), array(
'block-all-mixed-content',
'child-src',
'plugin-types',
));
}
private function getLevel1()
{
static $directives = array(
'default-src',
'connect-src',
'font-src',
'frame-src',
'img-src',
'media-src',
'object-src',
'sandbox',
'script-src',
'style-src',
'report-uri',
);
return $directives;
}
private function getLevel2()
{
static $directives = null;
if (null === $directives) {
$directives = array_merge($this->getLevel1(), array(
'base-uri',
'child-src',
'form-action',
'frame-ancestors',
'plugin-types',
));
}
return $directives;
}
private function getLevel3()
{
static $directives = null;
if (null === $directives) {
$directives = array_merge($this->getLevel2(), array(
'manifest-src',
'reflected-xss',
));
}
return $directives;
}
private function getDraftDirectives()
{
static $directives = array(
'block-all-mixed-content',
'upgrade-insecure-requests',
);
return $directives;
}
}
ContentSecurityPolicy/ShaComputer.php 0000666 00000004715 13244772066 0014057 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\ContentSecurityPolicy;
class ShaComputer
{
private $type;
private $favorite;
public function __construct($type)
{
if (!in_array($type, array('sha256', 'sha384', 'sha512'), true)) {
throw new \InvalidArgumentException(sprintf('Type "%s" is not supported', $type));
}
$this->type = $type;
}
public function computeForScript($html)
{
if (1 !== preg_match_all('/
{% endcspscript %}
// ...
{% cspstyle %}
{% endcspstyle %}
```
If you're not using Twig, you can use message digest with the `ContentSecurityPolicyListener`, it will automatically
compute the message digest and add it to the response CSP header:
```php
$listener->addScript("");
$listener->addStyle("");
```
#### Nonce for inline script handling
Content-Security-Policy specification also proposes a nonce implementation for inlining. Nelmio Security Bundle
comes out of the box with nonce functionality. Twig is natively supported.
In your Twig template use the `csp_nonce` function to access the nonce for the current request and add it to the response
CSP header. If you do not request a nonce, nonce will not be generated.
```twig
// ...
```
If you're not using Twig, you can use nonce functionality with the `ContentSecurityPolicyListener`:
```php
// generates a nonce at first time, returns the same nonce once generated
$listener->getNonce('script');
// or
$listener->getNonce('style');
```
#### Reporting:
Using the `report-uri` you can easily collect violation using the `ContentSecurityPolicyController`.
Here's an configuration example using `routing.yml`:
```yaml
csp_report:
path: /csp/report
methods: [POST]
defaults: { _controller: nelmio_security.csp_reporter_controller:indexAction }
```
This part of the configuration helps to filter noise collected by this endpoint:
```yaml
nelmio_security:
csp:
report_endpoint:
log_level: "notice" # Use the appropriate log_level
log_formatter: ~ # Declare a service name that must implement Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Log\LogFormatterInterface
log_channel: ~ # Declare the channel to use with the logger
filters:
# Filter false positive reports given a domain list
domains: true
# Filter false positive reports given a scheme list
schemes: true
# Filter false positive reports given known browser bugs
browser_bugs: true
# Filter false positive reports given known injected scripts
injected_scripts: true
# You can add you custom filter rules by implementing Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\NoiseDetectorInterface
# and tag the service with "nelmio_security.csp_report_filter"
dismiss:
# A list of key-values that should be dismissed
# A key is either a domain or a regular expression
# A value is a source or an array of source. The '*' wilcard is accepted
'/^data:/': 'script-src'
'/^https?:\/\/\d+\.\d+\.\d+\.\d+(:\d+)*/': '*'
'maxcdn.bootstrapcdn.com': '*'
'www.gstatic.com': ['media-src', 'img-src']
```
### **Signed Cookies**:
Ideally you should explicitly specify which cookies to sign. The reason for this is simple.
Cookies are sent with each request. Signatures are often longer than the cookie values themselves,
so signing everything would just needlessly slow down your app and increase bandwidth usage for
your users.
```yaml
nelmio_security:
signed_cookie:
names: [test1, test2]
```
However, for simplicity reasons, and to start with a high security and optimize later, you can
specify '*' as a cookie name to have all cookies signed automatically.
```
nelmio_security:
signed_cookie:
names: ['*']
```
Additional, optional configuration settings:
```yaml
nelmio_security:
signed_cookie:
secret: this_is_very_secret # defaults to global %secret% parameter
hash_algo: sha512 # defaults to sha256, see `hash_algos()` for available algorithms
```
### **Encrypted Cookies**:
**WARNING**: this service is now deprecated due to high coupling with deprecated mcrypt extension.
Encrypts the cookie values using `nelmio_security.encrypted_cookie.secret`. It works the same as
Signed Cookies:
```yaml
nelmio_security:
encrypted_cookie:
names: [test1, test2]
```
Additional, optional configuration settings:
```yaml
nelmio_security:
encrypted_cookie:
secret: this_is_very_secret # defaults to global %secret% parameter
algorithm: rijndael-256 # defaults to rijndael-128, see `mcrypt_list_algorithms()` for available algorithms
```
### **Clickjacking Protection**:
Most websites do not use frames and do not need to be frame-able. This is a common attack vector
for which all current browsers (IE8+, Opera10.5+, Safari4+, Chrome4+ and Firefox3.7+) have a
solution. An extra header sent by your site will tell the browser that it can not be displayed in
a frame. Browsers react by showing a short explanation instead of the content, or a blank page.
The valid values for the `X-Frame-Options` header are `DENY` (prevent framing from all pages) and
`SAMEORIGIN` (prevent framing from all pages not on the same domain). Additionally this bundle
supports the `ALLOW` option which skips the creation of the header for the matched URLs, if you
want to whitelist a few URLs and then DENY everything else.
One more option, as of yet [not well supported](https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options),
is to use `ALLOW-FROM uri` where `uri` can be any origin URL, from
`example.org` to `https://example.org:123/sub/path`. This lets you specify
exactly which domain can embed your site, in case you have a multi-domain setup.
Default configuration (deny everything):
```yaml
nelmio_security:
clickjacking:
paths:
'^/.*': DENY
content_types: []
```
Whitelist configuration (deny all but a few URLs):
```yaml
nelmio_security:
clickjacking:
paths:
'^/iframes/': ALLOW
'^/business/': 'ALLOW-FROM https://biz.example.org'
'^/local/': SAMEORIGIN
'^/.*': DENY
content_types: []
```
You can also of course only deny a few critical URLs, while leaving the rest alone:
```yaml
nelmio_security:
clickjacking:
paths:
'^/message/write': DENY
content_types: []
```
An optional `content_types` key lets you restrict the X-Frame-Options header only on some HTTP
response given their content type.
### **External Redirects Detection**:
This feature helps you detect and prevent redirects to external sites. This can easily happen
by accident if you carelessly take query parameters as redirection target.
You can log those (it's logged at warning level) by turning on logging:
```yaml
nelmio_security:
external_redirects:
log: true
```
You can abort (they are replaced by a 403 response) the redirects:
```yaml
nelmio_security:
external_redirects:
abort: true
```
Or you can override them, replacing the redirect's `Location` header by a route name or
another URL:
```yaml
# redirect to the 'home' route
nelmio_security:
external_redirects:
override: home
# redirect to another URL
nelmio_security:
external_redirects:
override: /foo
```
If you want to display the URL that was blocked on the overriding page you can
specify the `forward_as` parameter, which defines which query parameter will
receive the URL. For example using the config below, doing a redirect to
`http://example.org/` will be overridden to `/external-redirect?redirUrl=http://example.org/`.
```yaml
# redirect and forward the overridden URL
nelmio_security:
external_redirects:
override: /external-redirect
forward_as: redirUrl
```
Since it's quite common to have to redirect outside the website for legit reasons,
typically OAuth logins and such, you can whitelist a few domain names. All their subdomains
will be whitelisted as well, so that allows you to whitelist your own website's subdomains
if needed.
```yaml
nelmio_security:
external_redirects:
abort: true
whitelist:
- twitter.com
- facebook.com
```
### **Forced HTTPS/SSL Handling**:
By default, this option forces your entire site to use SSL, always. It redirect all users
reaching the site with a http:// URL to a https:// URL with a 302 response.
The base configuration for this is the following:
```yaml
nelmio_security:
forced_ssl: ~
```
If you turn this option on, it's recommended to also set your session cookie to be secure,
and all other cookies you send for that matter. You can do the former using:
```yaml
framework:
session:
cookie_secure: true
```
To keep a few URLs from being force-redirected to SSL you can define a whitelist of regular
expressions:
```yaml
nelmio_security:
forced_ssl:
enabled: true
whitelist:
- ^/unsecure/
```
To restrict the force-redirects to some hostnames only you can define a list of hostnames
as regular expressions:
```yaml
nelmio_security:
forced_ssl:
enabled: true
hosts:
- ^\.example\.org$
```
To change the way the redirect is done to a permanent redirect for example, you can set:
```yaml
nelmio_security:
forced_ssl:
enabled: true
redirect_status_code: 301
```
Then if you want to push it further, you can enable
[HTTP Strict Transport Security (HSTS)](http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02).
This is basically sending a header to tell the browser that your site must always be
accessed using SSL. If a user enters a http:// URL, the browser will convert it to https://
automatically, and will do so before making any request, which prevents man-in-the-middle
attacks.
The browser will cache the value for as long as the specified `hsts_max_age` (in seconds), and if
you turn on the `hsts_subdomains` option, the behavior will be applied to all subdomains as well.
```yaml
nelmio_security:
forced_ssl:
hsts_max_age: 2592000 # 30 days
hsts_subdomains: true
```
You can also tell the browser to add your site to the list of known HSTS sites, by enabling
`hsts_preload`. Once your site has appeared in the Chrome and Firefox preload lists, then new
users who come to your site will already be redirected to HTTPS URLs.
```yaml
nelmio_security:
forced_ssl:
hsts_max_age: 31536000 # 1 year
hsts_preload: true
```
> **Note:** A value of at least 1 year is currently required by [Chrome](https://hstspreload.org/)
> and [Firefox](https://blog.mozilla.org/security/2012/11/01/preloading-hsts/).
> `hsts_subdomains` must also be enabled for preloading to work.
You can speed up the inclusion process by submitting your site to the [HSTS Preload List](https://hstspreload.org/).
A small word of caution: While HSTS is great for security, it means that if the browser
can not establish your SSL certificate is valid, it will not allow the user to query your site.
That just means you should be careful and renew your certificate in due time.
Note: HSTS presently (Feb. 2018) works in Firefox 4+, Chrome 4+, Opera 12+, IE 11+, Edge 12+ and Safari 7+.
Check [caniuse](http://caniuse.com/#feat=stricttransportsecurity) for HSTS support in other browsers.
### **Flexible HTTPS/SSL Handling**:
The best way to handle SSL securely is to enable it for your entire site.
However in some cases this is not desirable, be it for caching or performance reasons,
or simply because most visitors of your site are anonymous and don't benefit much from the
added privacy and security of SSL.
If you don't want to enable SSL across the board, you need to avoid that people on insecure
networks (typically open Wi-Fi) get their session cookie stolen by sending it non-encrypted.
The way to achieve this is to set your session cookie to be secure as such - but don't do
it just yet, keep reading to the end.
```yaml
framework:
session:
cookie_secure: true
```
If you use the remember-me functionality, you would also mark that one as secure:
```yaml
security:
firewalls:
somename:
remember_me:
secure: true
```
Now if you do this, you have two problems. First, insecure pages will not be able to use
the session anymore, which can be inconvenient. Second, if a logged in user gets to a
non-HTTPS page of your site, it is seen as anonymous since his browser will not send the
session cookie. To fix this, this bundle sets a new insecure cookie
(`flexible_ssl.cookie_name`, defaults to `auth`) once a user logs in. That way, if any page
is accessed insecurely by a logged in user, he is redirected to the secure version of the
page, and his session is then visible to the framework.
Enabling the `flexible_ssl` option of the NelmioSecurityBundle will make sure that
logged-in users are always seeing secure pages, and it will make sure their session cookie
is secure, but anonymous users will still be able to have an insecure session, if you need
to use it to store non critical data like language settings and whatnot. The remember-me
cookie will also be made always secure, even if you leave the setting to false.
```yaml
nelmio_security:
flexible_ssl:
cookie_name: auth
unsecured_logout: false
```
You have to configure one more thing in your security configuration though: every firewall
should have our logout listener added, so that the special `auth` cookie can be cleared when
users log out. You can do it as such:
```yaml
security:
firewalls:
somename:
# ...
logout:
handlers:
- nelmio_security.flexible_ssl_listener
```
On logout, if you would like users to be redirected to an unsecure page set ``unsecured_logout``
to true.
### Cookie Session Handler:
You can configure the session handler to use a cookie based storage. There are various reasons to do this,
but generally speaking unless you have a very good one [you should avoid it](http://wonko.com/post/why-you-probably-shouldnt-use-cookies-to-store-session-data).
**WARNING**: The size limit of a cookie is 4KB, so make sure you are not storing objects or long
strings in the session.
```yaml
framework:
session:
handler_id: nelmio_security.session.handler
nelmio_security:
cookie_session:
enabled: true
name: session
```
### Content Type Sniffing
Disables the content type sniffing for script resources. Forces the browser to only execute script files with valid
content type headers. This is a non-standard header from Microsoft, more information can be found in
[their documentation at MSDN](http://msdn.microsoft.com/en-us/library/ie/gg622941.aspx).
```yaml
nelmio_security:
content_type:
nosniff: true
```
### XSS Protection
Enables or disables Microsoft XSS Protection on compatible browsers.
This is a non-standard header from Microsoft, more information can be found in
[their documentation at MSDN](http://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx).
```yaml
nelmio_security:
xss_protection:
enabled: true
mode_block: true
```
### Referrer Policy
Adds `Referrer-Policy` header to control the `Referer` header that is added
to requests made from your site, and for navigations away from your site by browsers.
You can specify multiple [referrer policies](https://www.w3.org/TR/referrer-policy/#referrer-policies).
The order of the policies is important. Browser will choose only the last policy they understand.
For example older browsers don’t understand the `strict-origin-when-cross-origin` policy.
A site can specify a `no-referrer` policy followed by a `strict-origin-when-cross-origin` policy:
older browsers will ignore the unknown `strict-origin-when-cross-origin` value and use `no-referrer`,
while newer browsers will use `strict-origin-when-cross-origin` because it is the last to be processed.
A referrer policy is:
* [`no-referrer`](https://www.w3.org/TR/referrer-policy/#referrer-policy-no-referrer),
* [`no-referrer-when-downgrade`](https://www.w3.org/TR/referrer-policy/#referrer-policy-no-referrer-when-downgrade),
* [`same-origin`](https://www.w3.org/TR/referrer-policy/#referrer-policy-same-origin),
* [`origin`](https://www.w3.org/TR/referrer-policy/#referrer-policy-origin),
* [`strict-origin`](https://www.w3.org/TR/referrer-policy/#referrer-policy-strict-origin),
* [`origin-when-cross-origin`](https://www.w3.org/TR/referrer-policy/#referrer-policy-origin-when-cross-origin),
* [`strict-origin-when-cross-origin`](https://www.w3.org/TR/referrer-policy/#referrer-policy-strict-origin-when-cross-origin),
* [`unsafe-url`](https://www.w3.org/TR/referrer-policy/#referrer-policy-unsafe-url),
* [the empty string](https://www.w3.org/TR/referrer-policy/#referrer-policy-empty-string).
For better security of your site please use `no-referrer`, `same-origin`, `strict-origin` or `strict-origin-when-cross-origin`.
```yaml
nelmio_security:
referrer_policy:
enabled: true
policies:
- 'no-referrer'
- 'strict-origin-when-cross-origin'
```
## License
Released under the MIT License, see LICENSE.
Resources/config/clickjacking.yml 0000666 00000000500 13244772066 0013134 0 ustar 00 services:
nelmio_security.clickjacking_listener:
class: Nelmio\SecurityBundle\EventListener\ClickjackingListener
arguments:
- '%nelmio_security.clickjacking.paths%'
- '%nelmio_security.clickjacking.content_types%'
tags:
- { name: kernel.event_subscriber }
Resources/config/content_type.yml 0000666 00000000465 13244772066 0013245 0 ustar 00 services:
nelmio_security.content_type_listener:
class: Nelmio\SecurityBundle\EventListener\ContentTypeListener
arguments:
- '%nelmio_security.content_type.nosniff%'
tags:
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }
Resources/config/cookie_session.yml 0000666 00000001332 13244772066 0013540 0 ustar 00 services:
nelmio_security.session.handler:
class: Nelmio\SecurityBundle\Session\CookieSessionHandler
arguments:
- '%nelmio_security.cookie_session.name%'
- '%nelmio_security.cookie_session.lifetime%'
- '%nelmio_security.cookie_session.path%'
- '%nelmio_security.cookie_session.domain%'
- '%nelmio_security.cookie_session.secure%'
- '%nelmio_security.cookie_session.httponly%'
- '@logger'
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 9998 }
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: -9998 }
Resources/config/csp.yml 0000666 00000006624 13244772066 0011322 0 ustar 00 parameters:
nelmio_security.nonce_generator.number_of_bytes: 16
services:
nelmio_security.ua_parser:
class: Nelmio\SecurityBundle\UserAgent\UserAgentParser
public: false
nelmio_security.ua_parser.ua_php:
class: Nelmio\SecurityBundle\UserAgent\UAFamilyParser\UAFamilyParser
public: true
arguments: ['@nelmio_security.ua_parser.ua_php.provider']
nelmio_security.ua_parser.ua_php.provider:
class: UAParser\Parser
public: false
factory: [UAParser\Parser, create]
nelmio_security.policy_manager:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\PolicyManager
nelmio_security.csp_listener:
class: Nelmio\SecurityBundle\EventListener\ContentSecurityPolicyListener
tags:
- { name: kernel.event_subscriber }
nelmio_security.csp_report.filter:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\Filter
nelmio_security.csp_report.filter.noise_detector_schemes:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\SchemesNoiseDetector
nelmio_security.csp_report.filter.noise_detector_domains_regex:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\DomainsRegexNoiseDetector
nelmio_security.csp_report.filter.noise_detector_domains:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\DomainsNoiseDetector
nelmio_security.csp_report.filter.noise_detector_custom_rules:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\CustomRulesNoiseDetector
arguments: [[]]
nelmio_security.csp_report.filter.noise_detector_injected_scripts:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\InjectedScriptsNoiseDetector
nelmio_security.csp_report.filter.noise_detector_browser_bugs:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\BrowserBugsNoiseDetector
arguments: ['@nelmio_security.ua_parser.ua_php.provider']
nelmio_security.csp_report.log_formatter:
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Log\LogFormatter
nelmio_security.csp_report.logger:
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Log\Logger
arguments: ['@logger', '@nelmio_security.csp_report.log_formatter', '%nelmio_security.csp.report_log_level%']
nelmio_security.csp_reporter_controller:
public: true
class: Nelmio\SecurityBundle\Controller\ContentSecurityPolicyController
arguments: ['@nelmio_security.csp_report.logger', '@event_dispatcher', '@nelmio_security.csp_report.filter']
nelmio_security.nonce_generator:
class: Nelmio\SecurityBundle\ContentSecurityPolicy\NonceGenerator
arguments: ["%nelmio_security.nonce_generator.number_of_bytes%"]
nelmio_security.sha_computer:
class: Nelmio\SecurityBundle\ContentSecurityPolicy\ShaComputer
arguments: ["%nelmio_security.csp.hash_algorithm%"]
nelmio_security.twig_extension:
public: false
class: Nelmio\SecurityBundle\Twig\NelmioCSPTwigExtension
arguments: ['@nelmio_security.csp_listener', '@nelmio_security.sha_computer']
tags:
- { name: "twig.extension" }
Resources/config/csp_legacy.yml 0000666 00000007026 13244772066 0012643 0 ustar 00 parameters:
nelmio_security.nonce_generator.number_of_bytes: 16
services:
nelmio_security.ua_parser:
class: Nelmio\SecurityBundle\UserAgent\UserAgentParser
public: false
nelmio_security.ua_parser.ua_php:
class: Nelmio\SecurityBundle\UserAgent\UAFamilyParser\UAFamilyParser
public: true
arguments: ['@nelmio_security.ua_parser.ua_php.provider']
nelmio_security.ua_parser.ua_php.provider:
class: UAParser\Parser
public: false
factory_class: UAParser\Parser
factory_method: create
nelmio_security.policy_manager:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\PolicyManager
nelmio_security.csp_listener:
class: Nelmio\SecurityBundle\EventListener\ContentSecurityPolicyListener
tags:
- { name: kernel.event_subscriber }
factory_class: Nelmio\SecurityBundle\EventListener\ContentSecurityPolicyListener
factory_method: fromConfig
nelmio_security.csp_report.filter:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\Filter
nelmio_security.csp_report.filter.noise_detector_schemes:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\SchemesNoiseDetector
nelmio_security.csp_report.filter.noise_detector_domains_regex:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\DomainsRegexNoiseDetector
nelmio_security.csp_report.filter.noise_detector_domains:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\DomainsNoiseDetector
nelmio_security.csp_report.filter.noise_detector_custom_rules:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\CustomRulesNoiseDetector
arguments: [[]]
nelmio_security.csp_report.filter.noise_detector_injected_scripts:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\InjectedScriptsNoiseDetector
nelmio_security.csp_report.filter.noise_detector_browser_bugs:
public: false
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Filter\BrowserBugsNoiseDetector
arguments: ['@nelmio_security.ua_parser.ua_php.provider']
nelmio_security.csp_report.log_formatter:
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Log\LogFormatter
nelmio_security.csp_report.logger:
class: Nelmio\SecurityBundle\ContentSecurityPolicy\Violation\Log\Logger
arguments: ['@logger', '@nelmio_security.csp_report.log_formatter', '%nelmio_security.csp.report_log_level%']
nelmio_security.csp_reporter_controller:
class: Nelmio\SecurityBundle\Controller\ContentSecurityPolicyController
arguments: ['@nelmio_security.csp_report.logger', '@event_dispatcher', '@nelmio_security.csp_report.filter']
nelmio_security.nonce_generator:
class: Nelmio\SecurityBundle\ContentSecurityPolicy\NonceGenerator
arguments: ["%nelmio_security.nonce_generator.number_of_bytes%"]
nelmio_security.sha_computer:
class: Nelmio\SecurityBundle\ContentSecurityPolicy\ShaComputer
arguments: ["%nelmio_security.csp.hash_algorithm%"]
nelmio_security.twig_extension:
public: false
class: Nelmio\SecurityBundle\Twig\NelmioCSPTwigExtension
arguments: ['@nelmio_security.csp_listener', '@nelmio_security.sha_computer']
tags:
- { name: "twig.extension" }
Resources/config/encrypted_cookie.yml 0000666 00000001262 13244772066 0014054 0 ustar 00 services:
nelmio_security.encrypted_cookie_listener:
class: Nelmio\SecurityBundle\EventListener\EncryptedCookieListener
arguments:
- '@nelmio_security.encrypter'
- '%nelmio_security.encrypted_cookie.names%'
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 9999 }
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: -9999 }
nelmio_security.encrypter:
class: Nelmio\SecurityBundle\Encrypter
arguments:
- '%nelmio_security.encrypter.secret%'
- '%nelmio_security.encrypter.algorithm%'
Resources/config/external_redirects.yml 0000666 00000002066 13244772066 0014417 0 ustar 00 parameters:
nelmio_security.external_redirects.whitelist: ~
services:
nelmio_security.external_redirect_listener:
class: Nelmio\SecurityBundle\EventListener\ExternalRedirectListener
arguments:
- '%nelmio_security.external_redirects.abort%'
- '%nelmio_security.external_redirects.override%'
- '%nelmio_security.external_redirects.forward_as%'
- '@?nelmio_security.external_redirect.target_validator'
- '@?logger'
- '@?router'
tags:
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }
- { name: monolog.logger, channel: security }
nelmio_security.external_redirect.target_validator:
alias: nelmio_security.external_redirect.target_validator.whitelist
nelmio_security.external_redirect.target_validator.whitelist:
public: false
class: Nelmio\SecurityBundle\ExternalRedirect\WhitelistBasedTargetValidator
arguments:
- '%nelmio_security.external_redirects.whitelist%'
Resources/config/flexible_ssl.yml 0000666 00000001013 13244772066 0013173 0 ustar 00 services:
nelmio_security.flexible_ssl_listener:
class: Nelmio\SecurityBundle\EventListener\FlexibleSslListener
arguments:
- '%nelmio_security.flexible_ssl.cookie_name%'
- '%nelmio_security.flexible_ssl.unsecured_logout%'
- '@event_dispatcher'
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 20000 }
- { name: kernel.event_listener, event: security.interactive_login, method: onLogin }
Resources/config/forced_ssl.yml 0000666 00000001145 13244772066 0012651 0 ustar 00 services:
nelmio_security.forced_ssl_listener:
class: Nelmio\SecurityBundle\EventListener\ForcedSslListener
arguments:
- '%nelmio_security.forced_ssl.hsts_max_age%'
- '%nelmio_security.forced_ssl.hsts_subdomains%'
- '%nelmio_security.forced_ssl.hsts_preload%'
- '%nelmio_security.forced_ssl.whitelist%'
- '%nelmio_security.forced_ssl.hosts%'
- '%nelmio_security.forced_ssl.redirect_status_code%'
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 30000 }
Resources/config/referrer_policy.yml 0000666 00000000477 13244772066 0013730 0 ustar 00 services:
nelmio_security.referrer_policy_listener:
class: Nelmio\SecurityBundle\EventListener\ReferrerPolicyListener
arguments:
- '%nelmio_security.referrer_policy.policies%'
tags:
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }
Resources/config/signed_cookie.yml 0000666 00000001234 13244772066 0013327 0 ustar 00 services:
nelmio_security.signed_cookie_listener:
class: Nelmio\SecurityBundle\EventListener\SignedCookieListener
arguments:
- '@nelmio_security.signer'
- '%nelmio_security.signed_cookie.names%'
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 10000 }
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: -10000 }
nelmio_security.signer:
class: Nelmio\SecurityBundle\Signer
arguments:
- '%nelmio_security.signer.secret%'
- '%nelmio_security.signer.hash_algo%'
Resources/config/xss_protection.yml 0000666 00000000427 13244772066 0013613 0 ustar 00 services:
nelmio_security.xss_protection_listener:
class: Nelmio\SecurityBundle\EventListener\XssProtectionListener
tags:
- { name: kernel.event_subscriber }
factory: [Nelmio\SecurityBundle\EventListener\XssProtectionListener, fromConfig]
Resources/config/xss_protection_legacy.yml 0000666 00000000462 13244772066 0015136 0 ustar 00 services:
nelmio_security.xss_protection_listener:
class: Nelmio\SecurityBundle\EventListener\XssProtectionListener
tags:
- { name: kernel.event_subscriber }
factory_class: Nelmio\SecurityBundle\EventListener\XssProtectionListener
factory_method: fromConfig
Session/CookieSessionHandler.php 0000666 00000012722 13244772066 0012776 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Session;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Psr\Log\LoggerInterface;
class CookieSessionHandler implements \SessionHandlerInterface
{
protected $request;
protected $response;
protected $cookieName;
protected $lifetime;
protected $path;
protected $domain;
protected $secure;
protected $httpOnly;
protected $cookie = false;
/**
* @param string $cookieName
* @param int $lifetime
* @param string $path
* @param string $domain
* @param bool $secure
* @param bool $httpOnly
* @param LoggerInterface $logger
*/
public function __construct($cookieName, $lifetime = 0, $path = '/', $domain = null, $secure = false, $httpOnly = true, LoggerInterface $logger = null)
{
$this->cookieName = $cookieName;
$this->path = $path;
$this->domain = $domain;
$this->lifetime = (int) $lifetime;
$this->secure = $secure;
$this->httpOnly = $httpOnly;
$this->logger = $logger;
}
/**
* @param FilterResponseEvent $e
*/
public function onKernelResponse(FilterResponseEvent $e)
{
if (HttpKernelInterface::MASTER_REQUEST !== $e->getRequestType()) {
return;
}
if ($this->logger) {
$this->logger->debug('CookieSessionHandler::onKernelResponse - Get the Response object');
}
$this->request->getSession()->save();
if ($this->cookie === false) {
if ($this->logger) {
$this->logger->debug('CookieSessionHandler::onKernelResponse - COOKIE not opened');
}
return;
}
if ($this->cookie === null) {
if ($this->logger) {
$this->logger->debug('CookieSessionHandler::onKernelResponse - CLEAR COOKIE');
}
$e->getResponse()->headers->clearCookie($this->cookieName);
} else {
$e->getResponse()->headers->setCookie($this->cookie);
}
}
/**
* @param GetResponseEvent $e
*/
public function onKernelRequest(GetResponseEvent $e)
{
if (HttpKernelInterface::MASTER_REQUEST !== $e->getRequestType()) {
return;
}
if ($this->logger) {
$this->logger->debug('CookieSessionHandler::onKernelRequest - Receiving the Request object');
}
$this->request = $e->getRequest();
}
/**
* {@inheritdoc}
*/
public function close()
{
return true;
}
/**
* {@inheritdoc}
*/
public function destroy($sessionId)
{
$this->cookie = null;
if ($this->logger) {
$this->logger->debug(sprintf('CookieSessionHandler::destroy sessionId=%s', $sessionId));
}
return true;
}
/**
* {@inheritdoc}
*/
public function gc($maxlifetime)
{
return true;
}
/**
* {@inheritdoc}
*/
public function open($savePath, $sessionId)
{
if (!$this->request) {
if ($this->logger) {
$this->logger->crit('CookieSessionHandler::open - The Request object is missing');
}
throw new \RuntimeException('You cannot access the session without a Request object set');
}
if ($this->logger) {
$this->logger->debug('CookieSessionHandler::open');
}
return true;
}
/**
* {@inheritdoc}
*/
public function read($sessionId)
{
if (!$this->request) {
if ($this->logger) {
$this->logger->crit('CookieSessionHandler::read - The Request object is missing');
}
throw new \RuntimeException('You cannot access the session without a Request object set');
}
if ($this->logger) {
$this->logger->debug(sprintf('CookieSessionHandler::read sessionId=%s', $sessionId));
}
if (!$this->request->cookies->has($this->cookieName)) {
return '';
}
$content = @unserialize($this->request->cookies->get($this->cookieName));
if ($content === false) {
$content = array(
'expire' => strtotime('now'),
'data' => '',
);
}
if ($content['expire'] !== 0 && $content['expire'] < strtotime('now')) {
return ''; // session expire
}
return $content['data'];
}
/**
* {@inheritdoc}
*/
public function write($sessionId, $sessionData)
{
if ($this->logger) {
$this->logger->debug(sprintf('CookieSessionHandler::write sessionId=%s', $sessionId));
}
$expire = $this->lifetime === 0 ? 0 : strtotime('now') + $this->lifetime;
$this->cookie = new Cookie(
$this->cookieName,
serialize(array('expire' => $expire, 'data' => $sessionData)),
$expire,
$this->path,
$this->domain,
$this->secure,
$this->httpOnly
);
return true;
}
}
Signer.php 0000666 00000004170 13244772066 0006525 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle;
class Signer
{
private $secret;
private $algo;
public function __construct($secret, $algo)
{
$this->secret = $secret;
$this->algo = $algo;
if (!in_array($this->algo, hash_algos(), true)) {
throw new \InvalidArgumentException(sprintf("The supplied hashing algorithm '%s' is not supported by this system.",
$this->algo));
}
}
public function getSignedValue($value, $signature = null)
{
if (null === $signature) {
$signature = $this->generateSignature($value);
}
return $value.'.'.$signature;
}
public function verifySignedValue($signedValue)
{
list($value, $signature) = $this->splitSignatureFromSignedValue($signedValue);
$signature2 = $this->generateSignature($value);
if (strlen($signature) !== strlen($signature2)) {
return false;
}
$result = 0;
for ($i = 0, $j = strlen($signature); $i < $j; ++$i) {
$result |= ord($signature[$i]) ^ ord($signature2[$i]);
}
return 0 === $result;
}
public function getVerifiedRawValue($signedValue)
{
if (!$this->verifySignedValue($signedValue)) {
throw new \InvalidArgumentException(sprintf("The signature for '%s' was invalid.", $signedValue));
}
$valueSignatureTuple = $this->splitSignatureFromSignedValue($signedValue);
return $valueSignatureTuple[0];
}
private function generateSignature($value)
{
return hash_hmac($this->algo, $value, $this->secret);
}
private function splitSignatureFromSignedValue($signedValue)
{
$pos = strrpos($signedValue, '.');
if (false === $pos) {
return array($signedValue, null);
}
return array(substr($signedValue, 0, $pos), substr($signedValue, $pos + 1));
}
}
Tests/ContentSecurityPolicy/ContentSecurityPolicyParserTest.php 0000666 00000002257 13244772066 0021265 0 ustar 00 parseSourceList($sourceList);
$this->assertEquals($expected, $result, 'CSP parser should quote CSP keywords');
}
public function keywordsProvider()
{
return array(
array('self', "'self'"),
array('none', "'none'"),
array('strict-dynamic', "'strict-dynamic'"),
array('unsafe-eval', "'unsafe-eval'"),
array('unsafe-inline', "'unsafe-inline'"),
array('hostname', 'hostname'),
array('example.com', 'example.com'),
array('http://example.com', 'http://example.com'),
array('http://example.com:81', 'http://example.com:81'),
array('https://example.com', 'https://example.com'),
);
}
}
Tests/ContentSecurityPolicy/DirectiveSetTest.php 0000666 00000051425 13244772066 0016161 0 ustar 00 createPolicyManager(), array('enforce' => array_merge(array('level1_fallback' => true), $directives)), 'enforce');
$request = new Request();
$request->headers->set('user-agent', $ua);
$this->assertSame($expected, $ds->buildHeaderValue($request));
}
private function createPolicyManager()
{
return new PolicyManager(new UserAgentParser(new UAFamilyParser(Parser::create())));
}
public function provideVariousConfig()
{
return array(
array(
'default-src example.org \'self\'; '.
'base-uri base-uri.example.org \'self\'; '.
'block-all-mixed-content; '.
'child-src child-src.example.org \'self\'; '.
'connect-src connect.example.org \'self\'; '.
'font-src font.example.org \'self\'; '.
'form-action form-action.example.org \'self\'; '.
'frame-ancestors frame-ancestors.example.org \'self\'; '.
'frame-src frame.example.org \'self\'; '.
'img-src img.example.org \'self\'; '.
'manifest-src manifest.example.org \'self\'; '.
'media-src media.example.org \'self\'; '.
'object-src object.example.org \'self\'; '.
'plugin-types application/shockwave-flash; '.
'script-src script.example.org \'self\'; '.
'style-src style.example.org \'self\'; '.
'upgrade-insecure-requests; '.
'report-uri http://report-uri',
self::UA_CHROME,
array(
'default-src' => array('example.org', "'self'"),
'script-src' => array('script.example.org', "'self'"),
'object-src' => array('object.example.org', "'self'"),
'style-src' => array('style.example.org', "'self'"),
'img-src' => array('img.example.org', "'self'"),
'manifest-src' => array('manifest.example.org', "'self'"),
'media-src' => array('media.example.org', "'self'"),
'frame-src' => array('frame.example.org', "'self'"),
'font-src' => array('font.example.org', "'self'"),
'connect-src' => array('connect.example.org', "'self'"),
'report-uri' => array('http://report-uri'),
'base-uri' => array('base-uri.example.org', "'self'"),
'child-src' => array('child-src.example.org', "'self'"),
'form-action' => array('form-action.example.org', "'self'"),
'frame-ancestors' => array('frame-ancestors.example.org', "'self'"),
'plugin-types' => array('application/shockwave-flash'),
'block-all-mixed-content' => true,
'upgrade-insecure-requests' => true,
),
),
array(
'default-src example.org \'self\'; '.
'base-uri base-uri.example.org \'self\'; '.
'connect-src connect.example.org \'self\'; '.
'font-src font.example.org \'self\'; '.
'form-action form-action.example.org \'self\'; '.
'frame-ancestors frame-ancestors.example.org \'self\'; '.
'frame-src frame.example.org \'self\'; '.
'img-src img.example.org \'self\'; '.
'manifest-src manifest.example.org \'self\'; '.
'media-src media.example.org \'self\'; '.
'object-src object.example.org \'self\'; '.
'script-src script.example.org \'self\'; '.
'style-src style.example.org \'self\'; '.
'upgrade-insecure-requests; '.
'report-uri http://report-uri',
self::UA_FIREFOX,
array(
'default-src' => array('example.org', "'self'"),
'script-src' => array('script.example.org', "'self'"),
'object-src' => array('object.example.org', "'self'"),
'style-src' => array('style.example.org', "'self'"),
'img-src' => array('img.example.org', "'self'"),
'media-src' => array('media.example.org', "'self'"),
'manifest-src' => array('manifest.example.org', "'self'"),
'frame-src' => array('frame.example.org', "'self'"),
'font-src' => array('font.example.org', "'self'"),
'connect-src' => array('connect.example.org', "'self'"),
'report-uri' => array('http://report-uri'),
'base-uri' => array('base-uri.example.org', "'self'"),
'child-src' => array('child-src.example.org', "'self'"),
'form-action' => array('form-action.example.org', "'self'"),
'frame-ancestors' => array('frame-ancestors.example.org', "'self'"),
'plugin-types' => array('application/shockwave-flash'),
'block-all-mixed-content' => true,
'upgrade-insecure-requests' => true,
),
),
array(
'default-src example.org \'self\'; '.
'base-uri base-uri.example.org \'self\'; '.
'block-all-mixed-content; '.
'child-src child-src.example.org \'self\'; '.
'connect-src connect.example.org \'self\'; '.
'font-src font.example.org \'self\'; '.
'form-action form-action.example.org \'self\'; '.
'frame-ancestors frame-ancestors.example.org \'self\'; '.
'frame-src frame.example.org \'self\'; '.
'img-src img.example.org \'self\'; '.
'media-src media.example.org \'self\'; '.
'object-src object.example.org \'self\'; '.
'plugin-types application/shockwave-flash; '.
'script-src script.example.org \'self\'; '.
'style-src style.example.org \'self\'; '.
'upgrade-insecure-requests; '.
'report-uri http://report-uri',
self::UA_IE,
array(
'default-src' => array('example.org', "'self'"),
'script-src' => array('script.example.org', "'self'"),
'object-src' => array('object.example.org', "'self'"),
'style-src' => array('style.example.org', "'self'"),
'img-src' => array('img.example.org', "'self'"),
'media-src' => array('media.example.org', "'self'"),
'frame-src' => array('frame.example.org', "'self'"),
'font-src' => array('font.example.org', "'self'"),
'connect-src' => array('connect.example.org', "'self'"),
'report-uri' => array('http://report-uri'),
'base-uri' => array('base-uri.example.org', "'self'"),
'child-src' => array('child-src.example.org', "'self'"),
'form-action' => array('form-action.example.org', "'self'"),
'frame-ancestors' => array('frame-ancestors.example.org', "'self'"),
'plugin-types' => array('application/shockwave-flash'),
'block-all-mixed-content' => true,
'upgrade-insecure-requests' => true,
),
),
array(
'default-src example.org \'self\'; '.
'base-uri base-uri.example.org \'self\'; '.
'block-all-mixed-content; '.
'child-src child-src.example.org \'self\'; '.
'connect-src connect.example.org \'self\'; '.
'font-src font.example.org \'self\'; '.
'form-action form-action.example.org \'self\'; '.
'frame-ancestors frame-ancestors.example.org \'self\'; '.
'frame-src frame.example.org \'self\'; '.
'img-src img.example.org \'self\'; '.
'manifest-src media.example.org \'self\'; '.
'media-src media.example.org \'self\'; '.
'object-src object.example.org \'self\'; '.
'plugin-types application/shockwave-flash; '.
'script-src script.example.org \'self\'; '.
'style-src style.example.org \'self\'; '.
'upgrade-insecure-requests; '.
'report-uri http://report-uri',
self::UA_OPERA,
array(
'default-src' => array('example.org', "'self'"),
'script-src' => array('script.example.org', "'self'"),
'object-src' => array('object.example.org', "'self'"),
'style-src' => array('style.example.org', "'self'"),
'img-src' => array('img.example.org', "'self'"),
'manifest-src' => array('media.example.org', "'self'"),
'media-src' => array('media.example.org', "'self'"),
'frame-src' => array('frame.example.org', "'self'"),
'font-src' => array('font.example.org', "'self'"),
'connect-src' => array('connect.example.org', "'self'"),
'report-uri' => array('http://report-uri'),
'base-uri' => array('base-uri.example.org', "'self'"),
'child-src' => array('child-src.example.org', "'self'"),
'form-action' => array('form-action.example.org', "'self'"),
'frame-ancestors' => array('frame-ancestors.example.org', "'self'"),
'plugin-types' => array('application/shockwave-flash'),
'block-all-mixed-content' => true,
'upgrade-insecure-requests' => true,
),
),
array(
'default-src example.org \'self\'; '.
'connect-src connect.example.org \'self\'; '.
'font-src font.example.org \'self\'; '.
'frame-src frame.example.org \'self\'; '.
'img-src img.example.org \'self\'; '.
'media-src media.example.org \'self\'; '.
'object-src object.example.org \'self\'; '.
'script-src script.example.org \'self\'; '.
'style-src style.example.org \'self\'; '.
'report-uri http://report-uri',
self::UA_SAFARI,
array(
'default-src' => array('example.org', "'self'"),
'script-src' => array('script.example.org', "'self'"),
'object-src' => array('object.example.org', "'self'"),
'style-src' => array('style.example.org', "'self'"),
'img-src' => array('img.example.org', "'self'"),
'media-src' => array('media.example.org', "'self'"),
'frame-src' => array('frame.example.org', "'self'"),
'font-src' => array('font.example.org', "'self'"),
'connect-src' => array('connect.example.org', "'self'"),
'report-uri' => array('http://report-uri'),
'base-uri' => array('base-uri.example.org', "'self'"),
'child-src' => array('child-src.example.org', "'self'"),
'form-action' => array('form-action.example.org', "'self'"),
'frame-ancestors' => array('frame-ancestors.example.org', "'self'"),
'plugin-types' => array('application/shockwave-flash'),
'block-all-mixed-content' => true,
'upgrade-insecure-requests' => true,
),
),
array(
'default-src example.org \'self\'; '.
'base-uri base-uri.example.org \'self\'; '.
'child-src child-src.example.org \'self\'; '.
'connect-src connect.example.org \'self\'; '.
'font-src font.example.org \'self\'; '.
'form-action form-action.example.org \'self\'; '.
'frame-ancestors frame-ancestors.example.org \'self\'; '.
'frame-src frame.example.org \'self\'; '.
'img-src img.example.org \'self\'; '.
'media-src media.example.org \'self\'; '.
'object-src object.example.org \'self\'; '.
'plugin-types application/shockwave-flash; '.
'script-src script.example.org \'self\'; '.
'style-src style.example.org \'self\'; '.
'report-uri http://report-uri',
self::UA_CHROME,
array(
'default-src' => array('example.org', "'self'"),
'script-src' => array('script.example.org', "'self'"),
'object-src' => array('object.example.org', "'self'"),
'style-src' => array('style.example.org', "'self'"),
'img-src' => array('img.example.org', "'self'"),
'media-src' => array('media.example.org', "'self'"),
'frame-src' => array('frame.example.org', "'self'"),
'font-src' => array('font.example.org', "'self'"),
'connect-src' => array('connect.example.org', "'self'"),
'report-uri' => array('http://report-uri'),
'base-uri' => array('base-uri.example.org', "'self'"),
'child-src' => array('child-src.example.org', "'self'"),
'form-action' => array('form-action.example.org', "'self'"),
'frame-ancestors' => array('frame-ancestors.example.org', "'self'"),
'plugin-types' => array('application/shockwave-flash'),
'block-all-mixed-content' => false,
'upgrade-insecure-requests' => false,
),
),
array(
'default-src example.org \'self\'; '.
'base-uri base-uri.example.org \'self\'; '.
'child-src child-src.example.org \'self\'; '.
'connect-src connect.example.org \'self\'; '.
'font-src font.example.org \'self\'; '.
'form-action form-action.example.org \'self\'; '.
'frame-ancestors frame-ancestors.example.org \'self\'; '.
'frame-src frame.example.org \'self\'; '.
'img-src img.example.org \'self\'; '.
'media-src media.example.org \'self\'; '.
'object-src object.example.org \'self\'; '.
'plugin-types application/shockwave-flash; '.
'script-src script.example.org \'self\'; '.
'style-src style.example.org \'self\'; '.
'report-uri http://report-uri',
self::UA_CHROME,
array(
'default-src' => array('example.org', "'self'"),
'script-src' => array('script.example.org', "'self'"),
'object-src' => array('object.example.org', "'self'"),
'style-src' => array('style.example.org', "'self'"),
'img-src' => array('img.example.org', "'self'"),
'media-src' => array('media.example.org', "'self'"),
'frame-src' => array('frame.example.org', "'self'"),
'font-src' => array('font.example.org', "'self'"),
'connect-src' => array('connect.example.org', "'self'"),
'report-uri' => array('http://report-uri'),
'base-uri' => array('base-uri.example.org', "'self'"),
'child-src' => array('child-src.example.org', "'self'"),
'form-action' => array('form-action.example.org', "'self'"),
'frame-ancestors' => array('frame-ancestors.example.org', "'self'"),
'plugin-types' => array('application/shockwave-flash'),
),
),
array(
'default-src \'none\'; '.
'base-uri \'none\'; '.
'form-action \'none\'; '.
'plugin-types \'none\'',
self::UA_CHROME,
array(
'default-src' => array('none'),
'plugin-types' => array('none'),
'base-uri' => array('none'),
'form-action' => array('none'),
),
),
array(
'default-src \'none\'; '.
'report-uri /csp/report1 /csp/report2',
self::UA_CHROME,
array(
'default-src' => array('none'),
'report-uri' => array('/csp/report1', '/csp/report2'),
),
),
);
}
/**
* @dataProvider provideConfigAndSignatures
*/
public function testBuildHeaderValueWithInlineSignatures($expected, $config, $signatures)
{
$directive = DirectiveSet::fromConfig(new PolicyManager(), $config, 'enforce');
$this->assertSame($expected, $directive->buildHeaderValue(new Request(), $signatures));
}
public function provideConfigAndSignatures()
{
return array(
array(
'default-src \'self\'; script-src \'self\' \'unsafe-inline\' \'sha-1\'; style-src \'self\' \'unsafe-inline\' \'sha2\'',
array(
'enforce' => array(
'level1_fallback' => true,
'default-src' => array("'self'"),
'script-src' => array("'self'", "'unsafe-inline'"),
'style-src' => array(),
),
),
array(
'script-src' => array('sha-1'),
'style-src' => array('sha2'),
),
),
array(
'default-src yolo; script-src yolo \'unsafe-inline\' \'sha-1\'; style-src yolo \'unsafe-inline\' \'sha2\'',
array(
'enforce' => array(
'level1_fallback' => true,
'default-src' => array('yolo'),
),
),
array(
'script-src' => array('sha-1'),
'style-src' => array('sha2'),
),
),
array(
'default-src \'self\'; script-src \'self\' \'unsafe-inline\' \'sha-1\'; style-src \'self\' \'unsafe-inline\' \'sha2\'',
array(
'enforce' => array(
'level1_fallback' => true,
'default-src' => array("'self'"),
'script-src' => array("'self'"),
'style-src' => array(),
),
),
array(
'script-src' => array('sha-1'),
'style-src' => array('sha2'),
),
),
array(
'default-src \'self\'; script-src \'self\' \'sha-1\'; style-src \'self\' \'sha2\'',
array(
'enforce' => array(
'level1_fallback' => false,
'default-src' => array("'self'"),
'script-src' => array("'self'"),
'style-src' => array(),
),
),
array(
'script-src' => array('sha-1'),
'style-src' => array('sha2'),
),
),
);
}
}
Tests/ContentSecurityPolicy/ShaComputerTest.php 0000666 00000005224 13244772066 0016015 0 ustar 00 assertSame($expected, $shaComputer->computeForScript($code));
}
public function provideValidScriptCode()
{
$mdMultiline = 'sha256-FJZognZIK0t5xLh8JBt4m/9rjpkYa4lTySrcUdRWHPM=';
$md = 'sha256-lClGOfcWqtQdAvO3zCRzZEg/4RmOMbr9/V54QO76j/A=';
return array(
array($mdMultiline, "
"),
array($md, ""),
array($md, ""),
array($md, ""),
array($md, ""),
);
}
/**
* @dataProvider provideValidStyleCode
*/
public function testComputeStyle($expected, $code)
{
$shaComputer = new ShaComputer('sha256');
$this->assertSame($expected, $shaComputer->computeForStyle($code));
}
public function provideValidStyleCode()
{
$mdMultiline = 'sha256-VbDrDAWYPqj9uExrJNmpK8bKIArMizR2+jcPhqSXO8M=';
$md = 'sha256-dmskSo+yqoLHXIXCFWnQJvCkjkJJmENqTDRi5+il2Yw=';
return array(
array($mdMultiline, '
'),
array($md, ''),
array($md, ''),
array($md, ''),
array($md, ''),
);
}
/**
* @expectedException \InvalidArgumentException
* @dataProvider provideInvalidScriptCode
*/
public function testComputeScriptShouldFail($code)
{
$shaComputer = new ShaComputer('sha256');
$shaComputer->computeForScript($code);
}
public function provideInvalidScriptCode()
{
return array(
array(' '),
array(' '), 'styles' => array('')), 3);
$this->assertEquals(
"default-src default.example.org 'self'; script-src default.example.org 'self' 'unsafe-inline' 'sha-script'; style-src default.example.org 'self' 'unsafe-inline' 'sha-style'",
$response->headers->get('Content-Security-Policy')
);
}
public function testWithContentTypeRestriction()
{
$listener = $this->buildSimpleListener(array('default-src' => "default.example.org 'self'"), false, true, array('text/html'));
$response = $this->callListener($listener, '/', true, 'application/json');
$this->assertEquals(null, $response->headers->get('Content-Security-Policy'));
}
public function testScript()
{
$script = "script.example.org 'self' 'unsafe-eval' 'strict-dynamic' 'unsafe-inline'";
$listener = $this->buildSimpleListener(array('script-src' => $script));
$response = $this->callListener($listener, '/', true);
$this->assertEquals(
"script-src script.example.org 'self' 'unsafe-eval' 'strict-dynamic' 'unsafe-inline'",
$response->headers->get('Content-Security-Policy')
);
}
public function testObject()
{
$object = "object.example.org 'self'";
$listener = $this->buildSimpleListener(array('object-src' => $object));
$response = $this->callListener($listener, '/', true);
$this->assertEquals("object-src object.example.org 'self'", $response->headers->get('Content-Security-Policy'));
}
public function testStyle()
{
$style = "style.example.org 'self'";
$listener = $this->buildSimpleListener(array('style-src' => $style));
$response = $this->callListener($listener, '/', true);
$this->assertEquals("style-src style.example.org 'self'", $response->headers->get('Content-Security-Policy'));
}
public function testImg()
{
$img = "img.example.org 'self'";
$listener = $this->buildSimpleListener(array('img-src' => $img));
$response = $this->callListener($listener, '/', true);
$this->assertEquals("img-src img.example.org 'self'", $response->headers->get('Content-Security-Policy'));
}
public function testMedia()
{
$media = "media.example.org 'self'";
$listener = $this->buildSimpleListener(array('media-src' => $media));
$response = $this->callListener($listener, '/', true);
$this->assertEquals("media-src media.example.org 'self'", $response->headers->get('Content-Security-Policy'));
}
public function testFrame()
{
$frame = "frame.example.org 'self'";
$listener = $this->buildSimpleListener(array('frame-src' => $frame));
$response = $this->callListener($listener, '/', true);
$this->assertEquals("frame-src frame.example.org 'self'", $response->headers->get('Content-Security-Policy'));
}
public function testFont()
{
$font = "font.example.org 'self'";
$listener = $this->buildSimpleListener(array('font-src' => $font));
$response = $this->callListener($listener, '/', true);
$this->assertEquals("font-src font.example.org 'self'", $response->headers->get('Content-Security-Policy'));
}
public function testConnect()
{
$connect = "connect.example.org 'self'";
$listener = $this->buildSimpleListener(array('connect-src' => $connect));
$response = $this->callListener($listener, '/', true);
$this->assertEquals(
"connect-src connect.example.org 'self'",
$response->headers->get('Content-Security-Policy')
);
}
public function testReportUri()
{
$reportUri = 'http://example.org/CSPReport';
$listener = $this->buildSimpleListener(array('report-uri' => $reportUri));
$response = $this->callListener($listener, '/', true);
$this->assertEquals(
'report-uri http://example.org/CSPReport',
$response->headers->get('Content-Security-Policy')
);
}
public function testEmpty()
{
$listener = $this->buildSimpleListener(array());
$response = $this->callListener($listener, '/', true);
$this->assertNull($response->headers->get('Content-Security-Policy'));
}
public function testAll()
{
$reportUri = 'http://example.org/CSPReport';
$listener = $this->buildSimpleListener(array(
'default-src' => "example.org 'self'",
'script-src' => "script.example.org 'self'",
'object-src' => "object.example.org 'self'",
'style-src' => "style.example.org 'self'",
'img-src' => "img.example.org 'self'",
'media-src' => "media.example.org 'self'",
'frame-src' => "frame.example.org 'self'",
'font-src' => "font.example.org 'self'",
'connect-src' => "connect.example.org 'self'",
'report-uri' => $reportUri,
'base-uri' => "base-uri.example.org 'self'",
'child-src' => "child-src.example.org 'self'",
'form-action' => "form-action.example.org 'self'",
'frame-ancestors' => "frame-ancestors.example.org 'self'",
'plugin-types' => 'application/shockwave-flash',
'block-all-mixed-content' => true,
'upgrade-insecure-requests' => true,
));
$response = $this->callListener($listener, '/', true);
$header = $response->headers->get('Content-Security-Policy');
$this->assertContains("default-src example.org 'self'", $header, 'Header should contain default-src');
$this->assertContains("script-src script.example.org 'self'", $header, 'Header should contain script-src');
$this->assertContains("object-src object.example.org 'self'", $header, 'Header should contain object-src');
$this->assertContains("style-src style.example.org 'self'", $header, 'Header should contain style-src');
$this->assertContains("img-src img.example.org 'self'", $header, 'Header should contain img-src');
$this->assertContains("media-src media.example.org 'self'", $header, 'Header should contain media-src');
$this->assertContains("frame-src frame.example.org 'self'", $header, 'Header should contain frame-src');
$this->assertContains("font-src font.example.org 'self'", $header, 'Header should contain font-src');
$this->assertContains("connect-src connect.example.org 'self'", $header, 'Header should contain connect-src');
$this->assertContains('report-uri http://example.org/CSPReport', $header, 'Header should contain report-uri');
$this->assertContains("base-uri base-uri.example.org 'self'", $header, 'Header should contain base-uri');
$this->assertContains("child-src child-src.example.org 'self'", $header, 'Header should contain child-src');
$this->assertContains("form-action form-action.example.org 'self'", $header, 'Header should contain form-action');
$this->assertContains("frame-ancestors frame-ancestors.example.org 'self'", $header, 'Header should contain frame-ancestors');
$this->assertContains('plugin-types application/shockwave-flash', $header, 'Header should contain plugin-types');
$this->assertContains('block-all-mixed-content', $header, 'Header should contain block-all-mixed-content');
$this->assertContains('upgrade-insecure-requests', $header, 'Header should contain upgrade-insecure-requests');
}
public function testDelimiter()
{
$spec = 'example.org';
$listener = $this->buildSimpleListener(array(
'default-src' => "default.example.org 'self'",
'script-src' => "script.example.org 'self'",
'object-src' => "object.example.org 'self'",
'style-src' => "style.example.org 'self'",
'img-src' => "img.example.org 'self'",
'media-src' => "media.example.org 'self'",
'frame-src' => "frame.example.org 'self'",
'font-src' => "font.example.org 'self'",
'connect-src' => "connect.example.org 'self'",
));
$response = $this->callListener($listener, '/', true);
$header = $response->headers->get('Content-Security-Policy');
$this->assertSame(
"default-src default.example.org 'self'; script-src script.example.org 'self'; ".
"object-src object.example.org 'self'; style-src style.example.org 'self'; ".
"img-src img.example.org 'self'; media-src media.example.org 'self'; ".
"frame-src frame.example.org 'self'; font-src font.example.org 'self'; ".
"connect-src connect.example.org 'self'",
$header,
'The header should contain all directives separated by a semicolon'
);
}
public function testAvoidDuplicates()
{
$spec = 'example.org';
$listener = $this->buildSimpleListener(array(
'default-src' => $spec,
'script-src' => $spec,
'object-src' => $spec,
'style-src' => $spec,
'img-src' => $spec,
'media-src' => $spec,
'frame-src' => $spec,
'font-src' => $spec,
'connect-src' => $spec,
));
$response = $this->callListener($listener, '/', true);
$header = $response->headers->get('Content-Security-Policy');
$this->assertEquals(
'default-src example.org',
$header,
'Response should contain only the default as the others are equivalent'
);
}
public function testVendorPrefixes()
{
$spec = 'example.org';
$listener = $this->buildSimpleListener(array(
'default-src' => $spec,
'script-src' => $spec,
'object-src' => $spec,
'style-src' => $spec,
'img-src' => $spec,
'media-src' => $spec,
'frame-src' => $spec,
'font-src' => $spec,
'connect-src' => $spec,
));
$response = $this->callListener($listener, '/', true);
$this->assertEquals(
$response->headers->get('Content-Security-Policy'),
$response->headers->get('X-Content-Security-Policy'),
'Response should contain non-standard X-Content-Security-Policy header'
);
}
public function testReportOnly()
{
$spec = 'example.org';
$listener = $this->buildSimpleListener(array(
'default-src' => $spec,
'script-src' => $spec,
'object-src' => $spec,
'style-src' => $spec,
'img-src' => $spec,
'media-src' => $spec,
'frame-src' => $spec,
'font-src' => $spec,
'connect-src' => $spec,
), true);
$response = $this->callListener($listener, '/', true);
$this->assertNull($response->headers->get('Content-Security-Policy'));
$this->assertNotNull($response->headers->get('Content-Security-Policy-Report-Only'));
}
public function testNoCompatHeaders()
{
$spec = 'example.org';
$listener = $this->buildSimpleListener(array(
'default-src' => $spec,
'script-src' => $spec,
'object-src' => $spec,
'style-src' => $spec,
'img-src' => $spec,
'media-src' => $spec,
'frame-src' => $spec,
'font-src' => $spec,
'connect-src' => $spec,
), false, false);
$response = $this->callListener($listener, '/', true);
$this->assertNull($response->headers->get('X-Content-Security-Policy'));
$this->assertNotNull($response->headers->get('Content-Security-Policy'));
}
public function testDirectiveSetUnset()
{
$directiveSet = new DirectiveSet(new PolicyManager());
$directiveSet->setDirectives(array('default-src' => 'foo'));
$this->assertEquals('default-src foo', $directiveSet->buildHeaderValue(new Request()));
$directiveSet->setDirective('default-src', '');
$this->assertEquals('', $directiveSet->buildHeaderValue(new Request()));
}
protected function buildSimpleListener(array $directives, $reportOnly = false, $compatHeaders = true, $contentTypes = array())
{
$directiveSet = new DirectiveSet(new PolicyManager());
$directiveSet->setDirectives($directives);
if ($reportOnly) {
return new ContentSecurityPolicyListener($directiveSet, new DirectiveSet(new PolicyManager()), $this->nonceGenerator, $this->shaComputer, $compatHeaders, $contentTypes);
} else {
return new ContentSecurityPolicyListener(new DirectiveSet(new PolicyManager()), $directiveSet, $this->nonceGenerator, $this->shaComputer, $compatHeaders, $contentTypes);
}
}
protected function callListener(ContentSecurityPolicyListener $listener, $path, $masterReq, $contentType = 'text/html', array $digestData = array(), $getNonce = 0)
{
$request = Request::create($path);
$event = new GetResponseEvent(
$this->kernel,
$request,
$masterReq ? HttpKernelInterface::MASTER_REQUEST : HttpKernelInterface::SUB_REQUEST
);
$listener->onKernelRequest($event);
if (isset($digestData['scripts'])) {
foreach ($digestData['scripts'] as $script) {
$listener->addScript($script);
}
}
if (isset($digestData['styles'])) {
foreach ($digestData['styles'] as $style) {
$listener->addStyle($style);
}
}
if (isset($digestData['signatures'])) {
foreach ($digestData['signatures'] as $type => $values) {
foreach ($values as $value) {
$listener->addSha($type, $value);
}
}
}
for ($i = 0; $i < $getNonce; ++$i) {
$listener->getNonce('script');
$listener->getNonce('style');
}
$response = new Response();
$response->headers->add(array('content-type' => $contentType));
$event = new FilterResponseEvent(
$this->kernel,
$request,
$masterReq ? HttpKernelInterface::MASTER_REQUEST : HttpKernelInterface::SUB_REQUEST,
$response
);
$listener->onKernelResponse($event);
return $response;
}
}
Tests/Listener/ContentTypeListenerTest.php 0000666 00000003174 13244772066 0015012 0 ustar 00 kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
}
public function testNoSniff()
{
$listener = new ContentTypeListener(true);
$response = $this->callListener($listener, '/', true);
$this->assertEquals(
'nosniff',
$response->headers->get('X-Content-Type-Options'),
'X-Content-Type-Options header should be present'
);
}
public function testEmpty()
{
$listener = new ContentTypeListener(false);
$response = $this->callListener($listener, '/', true);
$this->assertNull(
$response->headers->get('X-Content-Type-Options'),
'X-Content-Type-Options header should not be present'
);
}
protected function callListener(ContentTypeListener $listener, $path, $masterReq)
{
$request = Request::create($path);
$response = new Response();
$event = new FilterResponseEvent($this->kernel, $request, $masterReq ? HttpKernelInterface::MASTER_REQUEST : HttpKernelInterface::SUB_REQUEST, $response);
$listener->onKernelResponse($event);
return $response;
}
}
Tests/Listener/EncryptedCookieListenerTest.php 0000666 00000012205 13244772066 0015620 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Tests\Listener;
use Nelmio\SecurityBundle\Encrypter;
use Nelmio\SecurityBundle\EventListener\EncryptedCookieListener;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class EncryptedCookieListenerTest extends \PHPUnit\Framework\TestCase
{
private $encrypter;
private $kernel;
protected function setUp()
{
parent::setUp();
if (!function_exists('mcrypt_module_open')) {
$this->markTestSkipped('MCrypt is not installed');
}
if (PHP_VERSION_ID >= 70100 ) {
$this->markTestSkipped('MCrypt is deprecated since PHP 7.1');
}
$this->encrypter = new Encrypter('secret', 'rijndael-128');
$this->kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
}
/**
* @dataProvider provideCookieReading
* @group legacy
* @expectedDeprecation Encrypted Cookie is now deprecated due to high coupling with the deprecated mcrypt extension, support will be removed in NelmioSecurityBundle version 3
*/
public function testCookieReading($encryptedCookieNames, $inputCookies, $expectedCookies)
{
$listener = new EncryptedCookieListener($this->encrypter, $encryptedCookieNames);
$request = Request::create('/', 'GET', array(), $inputCookies);
$event = new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST);
$listener->onKernelRequest($event);
$this->assertSame($expectedCookies, $request->cookies->all());
}
public function provideCookieReading()
{
return array(
array(array(), array(), array()),
array(array(), array('foo' => 'bar'), array('foo' => 'bar')),
array(array('foo'), array('foo' => 'BX9knwx1sPhIGxTxukVfsA0o0m/KRm4kMwwEYn/etMw'), array('foo' => 'bar')),
array(array('*'), array('foo' => 'yNrfCriKHLxUtuZUInRlNsOjbLcL5a/4M8oDDXzt2aI'), array('foo' => 'bar')),
);
}
/**
* @dataProvider provideCookieWritingWithoutEncryption
* @group legacy
* @expectedDeprecation Encrypted Cookie is now deprecated due to high coupling with the deprecated mcrypt extension, support will be removed in NelmioSecurityBundle version 3
*/
public function testCookieWritingWithoutEncryption($encryptedCookieNames, $inputCookies, $expectedCookies)
{
$listener = new EncryptedCookieListener($this->encrypter, $encryptedCookieNames);
$request = Request::create('/');
$response = new Response();
foreach ($inputCookies as $name => $cookie) {
$response->headers->setCookie(new Cookie($name, $cookie));
}
$event = new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response);
$listener->onKernelResponse($event);
$responseCookieValues = array();
foreach ($response->headers->getCookies() as $cookie) {
$responseCookieValues[$cookie->getName()] = $cookie->getValue();
}
$this->assertSame($expectedCookies, $responseCookieValues);
}
public function provideCookieWritingWithoutEncryption()
{
return array(
array(array(), array(), array()),
array(array(), array('foo' => 'bar'), array('foo' => 'bar')),
);
}
/**
* @group legacy
* @expectedDeprecation Encrypted Cookie is now deprecated due to high coupling with the deprecated mcrypt extension, support will be removed in NelmioSecurityBundle version 3
*/
public function testCookieWritingWithEncryption()
{
$inputCookies = array(
'foo' => 'bar',
'symfony' => 'rocks',
);
$listener = new EncryptedCookieListener($this->encrypter, array('*'));
$request = Request::create('/');
$response = new Response();
foreach ($inputCookies as $name => $cookie) {
$response->headers->setCookie(new Cookie($name, $cookie));
}
$event = new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response);
$listener->onKernelResponse($event);
$responseCookieValues = array();
foreach ($response->headers->getCookies() as $cookie) {
$responseCookieValues[$cookie->getName()] = $cookie->getValue();
}
$this->assertNotSame($inputCookies, $responseCookieValues);
$request = Request::create('/', 'GET', array(), $responseCookieValues);
$event = new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST);
$listener->onKernelRequest($event);
$this->assertSame($inputCookies, $request->cookies->all());
}
}
Tests/Listener/ExternalRedirectListenerTest.php 0000666 00000013077 13244772066 0016005 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Tests\Listener;
use Nelmio\SecurityBundle\EventListener\ExternalRedirectListener;
use Nelmio\SecurityBundle\ExternalRedirect\WhitelistBasedTargetValidator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
class ExternalRedirectListenerTest extends \PHPUnit\Framework\TestCase
{
private $kernel;
protected function setUp()
{
$this->kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
}
/**
* @dataProvider provideRedirectMatcher
*/
public function testRedirectMatcher($source, $target, $expected)
{
$listener = new ExternalRedirectListener(true);
$result = $listener->isExternalRedirect($source, $target);
$this->assertSame($expected, $result);
}
public function provideRedirectMatcher()
{
return array(
// internal
array('http://test.org/', 'http://test.org/foo', false),
array('http://test.org/', 'https://test.org/foo', false),
array('http://test.org/', '/foo', false),
array('http://test.org/', 'foo', false),
// external
array('http://test.org/', 'http://example.org/foo', true),
array('http://test.org/', 'http://foo.test.org/', true),
array('http://test.org/', 'http://test.org.com/', true),
array('http://test.org/', 'http://foo.com/http://test.org/', true),
array('http://test.org/', '//foo.com/', true),
array('http://test.org/', "\r".'http://foo.com/', true),
array('http://test.org/', "\0\0".'http://foo.com/', true),
array('http://test.org/', " ".'http://foo.com/', true),
);
}
/**
* @depends testRedirectMatcher
* @expectedException Symfony\Component\HttpKernel\Exception\HttpException
*/
public function testRedirectAbort()
{
$listener = new ExternalRedirectListener(true);
$this->filterResponse($listener, 'http://foo.com/', 'http://bar.com/');
}
/**
* @depends testRedirectMatcher
*/
public function testRedirectOverrides()
{
$listener = new ExternalRedirectListener(false, '/override');
$response = $this->filterResponse($listener, 'http://foo.com/', 'http://bar.com/');
$this->assertSame(true, $response->isRedirect());
$this->assertSame('/override', $response->headers->get('Location'));
}
/**
* @depends testRedirectMatcher
*/
public function testRedirectSkipsAllowedTargets()
{
$listener = new ExternalRedirectListener(true, null, null, new WhitelistBasedTargetValidator(array('bar.com')));
$response = $this->filterResponse($listener, 'http://foo.com/', 'http://bar.com');
$this->assertTrue($response->isRedirect());
}
/**
* @depends testRedirectMatcher
* @dataProvider provideRedirectWhitelistsFailing
* @expectedException Symfony\Component\HttpKernel\Exception\HttpException
*/
public function testRedirectDoesNotSkipNonWhitelistedDomains($whitelist, $domain)
{
$listener = new ExternalRedirectListener(true, null, null, $whitelist);
$this->filterResponse($listener, 'http://foo.com/', 'http://'.$domain.'/');
}
public function provideRedirectWhitelistsFailing()
{
return array(
array(array('bar.com', 'baz.com'), 'abaz.com'),
array(array('bar.com', 'baz.com'), 'moo.com'),
array(array('.co.uk'), 'telco.uk'),
array(array(), 'bar.com'),
);
}
/**
* @depends testRedirectMatcher
* @dataProvider provideRedirectWhitelistsPassing
*/
public function testRedirectSkipsWhitelistedDomains($whitelist, $domain)
{
$listener = new ExternalRedirectListener(true, null, null, $whitelist);
$response = $this->filterResponse($listener, 'http://foo.com/', 'http://'.$domain.'/');
$this->assertTrue($response->isRedirect());
}
public function provideRedirectWhitelistsPassing()
{
return array(
array(array('bar.com', 'baz.com'), 'bar.com'),
array(array('bar.com', 'baz.com'), '.baz.com'),
array(array('bar.com', 'baz.com'), 'foo.baz.com'),
array(array('.co.uk'), 'tel.co.uk'),
array(array(), ''),
);
}
public function testListenerSkipsSubReqs()
{
$listener = new ExternalRedirectListener(true);
$request = Request::create('http://test.org/');
$response = new RedirectResponse('http://foo.com/');
$event = new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::SUB_REQUEST, $response);
$listener->onKernelResponse($event);
$this->assertSame(true, $response->isRedirect());
$this->assertSame('http://foo.com/', $response->headers->get('Location'));
}
protected function filterResponse($listener, $source, $target)
{
$request = Request::create($source);
$response = new RedirectResponse($target);
$event = new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response);
$listener->onKernelResponse($event);
return $response;
}
}
Tests/Listener/FlexibleSslListenerTest.php 0000666 00000012550 13244772066 0014750 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Tests\Listener;
use Nelmio\SecurityBundle\EventListener\FlexibleSslListener;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
class FlexibleSslListenerTest extends \PHPUnit\Framework\TestCase
{
private $kernel;
private $dispatcher;
private $listener;
protected function setUp()
{
$this->kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
$this->dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock();
$this->listener = new FlexibleSslListener('auth', false, $this->dispatcher);
}
public function testKernelRequestWithNonAuthedNonSslRequest()
{
$request = Request::create('http://localhost/');
$event = new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST);
$this->listener->onKernelRequest($event);
$this->assertFalse($event->hasResponse());
}
public function testKernelRequestWithAuthedNonSslRequest()
{
$request = Request::create('http://localhost/');
$request->cookies->set('auth', '1');
$event = new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST);
$this->listener->onKernelRequest($event);
$this->assertTrue($event->hasResponse());
$this->assertTrue($event->getResponse()->isRedirection());
}
public function testKernelRequestWithNonAuthedSslRequest()
{
$request = Request::create('https://localhost/');
$event = new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST);
$this->listener->onKernelRequest($event);
$this->assertFalse($event->hasResponse());
}
public function testKernelRequestWithAuthedSslRequest()
{
$request = Request::create('https://localhost/');
$request->cookies->set('auth', '1');
$event = new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST);
$this->listener->onKernelRequest($event);
$this->assertFalse($event->hasResponse());
}
public function testPostLoginKernelResponse()
{
$request = Request::create('https://localhost/');
$response = new Response();
$event = new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response);
$this->listener->onPostLoginKernelResponse($event);
$cookies = $response->headers->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
$this->assertTrue(isset($cookies['']['/']['auth']));
$this->assertSame('1', $cookies['']['/']['auth']->getValue());
$this->assertFalse($cookies['']['/']['auth']->isSecure());
$this->assertTrue(isset($cookies['']['/'][session_name()]));
$this->assertSame(session_id(), $cookies['']['/'][session_name()]->getValue());
$this->assertTrue($cookies['']['/'][session_name()]->isSecure());
}
public function testKernelRequestSkipsSubReqs()
{
$request = Request::create('http://localhost/');
$request->cookies->set('auth', '1');
$event = new GetResponseEvent($this->kernel, $request, HttpKernelInterface::SUB_REQUEST);
$this->listener->onKernelRequest($event);
$this->assertFalse($event->hasResponse());
}
public function testPostLoginKernelResponseSkipsSubReqs()
{
$request = Request::create('https://localhost/');
$response = new Response();
$event = new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::SUB_REQUEST, $response);
$this->listener->onPostLoginKernelResponse($event);
$cookies = $response->headers->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
$this->assertFalse(isset($cookies['']['/']['auth']));
}
public function testSecureLogout()
{
$response = new RedirectResponse('https://foo');
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock();
$token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock();
$this->listener->logout($request, $response, $token);
$this->assertSame('https://foo', $response->headers->get('Location'));
}
public function testUnsecuredLogout()
{
$unsecuredLogoutListener = new FlexibleSslListener('auth', true, $this->dispatcher);
$response = new RedirectResponse('https://foo');
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock();
$token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock();
$unsecuredLogoutListener->logout($request, $response, $token);
$this->assertSame('http://foo', $response->headers->get('Location'));
}
}
Tests/Listener/ForcedSslListenerTest.php 0000666 00000011630 13244772066 0014416 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Tests\Listener;
use Nelmio\SecurityBundle\EventListener\ForcedSslListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class ForcedSslListenerTest extends \PHPUnit\Framework\TestCase
{
private $kernel;
protected function setUp()
{
$this->kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
}
/**
* @dataProvider provideHstsHeaders
*/
public function testHstsHeaders($hstsMaxAge, $hstsSubdomains, $hstsPreload, $result)
{
$listener = new ForcedSslListener($hstsMaxAge, $hstsSubdomains, $hstsPreload);
$response = $this->callListenerResp($listener, 'https://localhost/', true);
$this->assertSame($result, $response->headers->get('Strict-Transport-Security'));
}
/**
* @dataProvider provideHstsHeaders
*/
public function testHstsHeadersNotSetForNonSecureRequest($hstsMaxAge, $hstsSubdomains, $hstsPreload)
{
$listener = new ForcedSslListener($hstsMaxAge, $hstsSubdomains, $hstsPreload);
$response = $this->callListenerResp($listener, 'http://localhost/', true);
$this->assertSame(null, $response->headers->get('Strict-Transport-Security'));
}
public function provideHstsHeaders()
{
return array(
array(60, true, false, 'max-age=60; includeSubDomains'),
array(60, false, false, 'max-age=60'),
array(3600, true, false, 'max-age=3600; includeSubDomains'),
array(3600, false, false, 'max-age=3600'),
array(3600, true, true, 'max-age=3600; includeSubDomains; preload'),
array(3600, false, true, 'max-age=3600; preload'),
);
}
public function testForcedSslSkipsSubReqs()
{
$listener = new ForcedSslListener(60, true);
$response = $this->callListenerResp($listener, 'https://localhost/', false);
$this->assertSame(null, $response->headers->get('Strict-Transport-Security'));
}
public function testForcedSslSkipsWhitelisted()
{
$listener = new ForcedSslListener(60, true, false, array('^/foo/', 'bar'));
$response = $this->callListenerReq($listener, 'http://localhost/foo/lala', true);
$this->assertSame(null, $response);
$response = $this->callListenerReq($listener, 'http://localhost/lala/foo/lala', true);
$this->assertSame('https://localhost/lala/foo/lala', $response->headers->get('Location'));
$response = $this->callListenerReq($listener, 'https://localhost/lala/abarb', true);
$this->assertSame(null, $response);
}
public function testForcedSslOnlyUsesHosts()
{
$listener = new ForcedSslListener(60, true, false, array(), array('^foo\.com$', '\.example\.org$'));
$response = $this->callListenerReq($listener, 'http://afoo.com/foo/lala', true);
$this->assertSame(null, $response);
$response = $this->callListenerReq($listener, 'http://foo.com/foo/lala', true);
$this->assertSame('https://foo.com/foo/lala', $response->headers->get('Location'));
$response = $this->callListenerReq($listener, 'http://test.example.org/foo/lala', true);
$this->assertSame('https://test.example.org/foo/lala', $response->headers->get('Location'));
}
public function testForcedSslRedirectStatusCodes()
{
$listener = new ForcedSslListener(null, false);
$response = $this->callListenerReq($listener, '/foo/lala', true);
$this->assertSame(302, $response->getStatusCode());
$listener = new ForcedSslListener(null, false, false, array(), array(), 301);
$response = $this->callListenerReq($listener, '/foo/lala', true);
$this->assertSame(301, $response->getStatusCode());
}
protected function callListenerReq($listener, $uri, $masterReq)
{
$request = Request::create($uri);
$event = new GetResponseEvent($this->kernel, $request, $masterReq ? HttpKernelInterface::MASTER_REQUEST : HttpKernelInterface::SUB_REQUEST);
$listener->onKernelRequest($event);
return $event->getResponse();
}
protected function callListenerResp(ForcedSslListener $listener, $uri, $masterReq)
{
$request = Request::create($uri);
$response = new Response();
$event = new FilterResponseEvent($this->kernel, $request, $masterReq ? HttpKernelInterface::MASTER_REQUEST : HttpKernelInterface::SUB_REQUEST, $response);
$listener->onKernelResponse($event);
return $response;
}
}
Tests/Listener/ReferrerPolicyListenerTest.php 0000666 00000004157 13244772066 0015474 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Tests\Listener;
use Nelmio\SecurityBundle\EventListener\ReferrerPolicyListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class ReferrerPolicyListenerTest extends \PHPUnit\Framework\TestCase
{
private $kernel;
protected function setUp()
{
$this->kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
}
/**
* @dataProvider provideVariousConfigs
*/
public function testVariousConfig($expectedValue, $listener)
{
$response = $this->callListener($listener, '/', true);
$this->assertEquals($expectedValue, $response->headers->get('Referrer-Policy'));
}
public function provideVariousConfigs()
{
return array(
array('', new ReferrerPolicyListener(array())),
array('no-referrer', new ReferrerPolicyListener(array('no-referrer'))),
array('no-referrer, strict-origin-when-cross-origin', new ReferrerPolicyListener(array('no-referrer', 'strict-origin-when-cross-origin'))),
array('no-referrer, no-referrer-when-downgrade, strict-origin-when-cross-origin', new ReferrerPolicyListener(array('no-referrer', 'no-referrer-when-downgrade', 'strict-origin-when-cross-origin'))),
);
}
protected function callListener(ReferrerPolicyListener $listener, $path, $masterReq)
{
$request = Request::create($path);
$response = new Response();
$event = new FilterResponseEvent(
$this->kernel,
$request,
$masterReq ? HttpKernelInterface::MASTER_REQUEST : HttpKernelInterface::SUB_REQUEST,
$response
);
$listener->onKernelResponse($event);
return $response;
}
}
Tests/Listener/SignedCookieListenerTest.php 0000666 00000010554 13244772066 0015101 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Tests\Listener;
use Nelmio\SecurityBundle\Signer;
use Nelmio\SecurityBundle\EventListener\SignedCookieListener;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class SignedCookieListenerTest extends \PHPUnit\Framework\TestCase
{
private $signer;
private $kernel;
protected function setUp()
{
$this->signer = new Signer('secret', 'sha1');
$this->kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
}
/**
* @dataProvider provideCookieReading
*/
public function testCookieReading($signedCookieNames, $inputCookies, $expectedCookies)
{
$listener = new SignedCookieListener($this->signer, $signedCookieNames);
$request = Request::create('/', 'GET', array(), $inputCookies);
$event = new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST);
$listener->onKernelRequest($event);
$this->assertSame($expectedCookies, $request->cookies->all());
}
public function provideCookieReading()
{
return array(
array(array(), array(), array()),
array(array(), array('foo' => 'bar'), array('foo' => 'bar')),
array(array('foo'), array('foo' => 'bar'), array()),
array(array('foo'), array('foo' => 'bar.ca3756f81d3728a023bdc8a622c0906f373b795e'), array('foo' => 'bar')),
array(array('*'), array('foo' => 'bar.ca3756f81d3728a023bdc8a622c0906f373b795e'), array('foo' => 'bar')),
);
}
/**
* @dataProvider provideCookieWriting
*/
public function testCookieWriting($signedCookieNames, $inputCookies, $expectedCookies)
{
$listener = new SignedCookieListener($this->signer, $signedCookieNames);
$request = Request::create('/');
$response = new Response();
foreach ($inputCookies as $name => $cookie) {
$response->headers->setCookie(new Cookie($name, $cookie));
}
$event = new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response);
$listener->onKernelResponse($event);
$responseCookieValues = array();
foreach ($response->headers->getCookies() as $cookie) {
$responseCookieValues[$cookie->getName()] = $cookie->getValue();
}
$this->assertSame($expectedCookies, $responseCookieValues);
}
public function provideCookieWriting()
{
return array(
array(array(), array(), array()),
array(array(), array('foo' => 'bar'), array('foo' => 'bar')),
array(array('foo'), array('foo' => 'bar'), array('foo' => 'bar.ca3756f81d3728a023bdc8a622c0906f373b795e')),
array(array('*'), array('foo' => 'bar'), array('foo' => 'bar.ca3756f81d3728a023bdc8a622c0906f373b795e')),
);
}
public function testCookieReadingSkipsSubReqs()
{
$listener = new SignedCookieListener($this->signer, array('*'));
$request = Request::create('/', 'GET', array(), array('foo' => 'bar'));
$event = new GetResponseEvent($this->kernel, $request, HttpKernelInterface::SUB_REQUEST);
$listener->onKernelRequest($event);
$this->assertEquals('bar', $request->cookies->get('foo'));
}
public function testCookieWritingSkipsSubReqs()
{
$listener = new SignedCookieListener($this->signer, array('*'));
$request = Request::create('/');
$response = new Response();
$response->headers->setCookie(new Cookie('foo', 'bar'));
$event = new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::SUB_REQUEST, $response);
$listener->onKernelResponse($event);
$cookies = $response->headers->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
$this->assertEquals('bar', $cookies['']['/']['foo']->getValue());
}
}
Tests/Listener/XssProtectionListenerTest.php 0000666 00000003241 13244772066 0015355 0 ustar 00 kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
}
/**
* @dataProvider provideVariousConfigs
*/
public function testVariousConfig($expectedValue, $listener)
{
$response = $this->callListener($listener, '/', true);
$this->assertEquals($expectedValue, $response->headers->get('X-Xss-Protection'));
}
public function provideVariousConfigs()
{
return array(
array('0', new XssProtectionListener(false, false)),
array('1', new XssProtectionListener(true, false)),
array('0', new XssProtectionListener(false, true)),
array('1; mode=block', new XssProtectionListener(true, true)),
);
}
protected function callListener(XssProtectionListener $listener, $path, $masterReq)
{
$request = Request::create($path);
$response = new Response();
$event = new FilterResponseEvent(
$this->kernel,
$request,
$masterReq ? HttpKernelInterface::MASTER_REQUEST : HttpKernelInterface::SUB_REQUEST,
$response
);
$listener->onKernelResponse($event);
return $response;
}
}
Tests/Session/CookieSessionHandlerTest.php 0000666 00000010601 13244772066 0014732 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Tests\Session;
use Nelmio\SecurityBundle\Session\CookieSessionHandler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class CookieSessionHandlerTest extends \PHPUnit\Framework\TestCase
{
private $handler;
private $kernel;
public function setUp()
{
$this->handler = new CookieSessionHandler('s');
$this->kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
}
/**
* @expectedException RuntimeException
*/
public function testOpenWithNoRequest()
{
$this->handler->open('foo', 'bar');
}
/**
* @expectedException RuntimeException
*/
public function testReadWithNoRequest()
{
$this->handler->read('foo');
}
public function testOpenWithoutSessionCookie()
{
$request = new Request();
$response = new Response();
$session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')->getMock();
$session->expects($this->exactly(1))->method('save');
$request->setSession($session);
$this->handler->onKernelRequest(new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST));
$this->assertTrue($this->handler->open('foo', 'bar'));
$this->handler->write('sessionId', 'mydata');
$this->handler->onKernelResponse(new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response));
$cookies = $response->headers->getCookies();
$this->assertEquals(1, count($cookies));
$this->assertEquals('a:2:{s:6:"expire";i:0;s:4:"data";s:6:"mydata";}', $cookies[0]->getValue());
$this->assertEquals('s', $cookies[0]->getName());
}
public function testWriteDestroy()
{
$this->handler->write('sessionId', 'mydata');
$request = new Request();
$response = new Response();
$session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')->getMock();
$session->expects($this->exactly(2))->method('save');
$request->setSession($session);
$this->handler->onKernelRequest(new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST));
$this->handler->onKernelResponse(new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response));
$cookies = $response->headers->getCookies();
$this->assertEquals(1, count($cookies));
$this->assertEquals('a:2:{s:6:"expire";i:0;s:4:"data";s:6:"mydata";}', $cookies[0]->getValue());
$this->assertEquals('s', $cookies[0]->getName());
$this->handler->destroy('sessionId');
$this->handler->onKernelResponse(new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response));
$cookies = $response->headers->getCookies();
$this->assertEquals(1, count($cookies));
$this->assertEquals('', $cookies[0]->getValue());
$this->assertEquals('s', $cookies[0]->getName());
}
/**
* Cookie not opened.
*/
public function testCookieNotOpened()
{
$session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')->getMock();
$headers = $this->getMockBuilder('Symfony\Component\HttpFoundation\ResponseHeaderBag')->getMock();
$headers
->expects($this->any())
->method('clearCookie');
$headers
->expects($this->any())
->method('setCookie');
$response = new Response();
$request = new Request();
$request->setSession($session);
$response->headers = $headers;
$this->handler->onKernelRequest(new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST));
$this->handler->onKernelResponse(new FilterResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response));
}
}
Tests/SignerTest.php 0000666 00000002726 13244772066 0010474 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Tests;
use Nelmio\SecurityBundle\Signer;
class SignerTest extends \PHPUnit\Framework\TestCase
{
/**
* @expectedException InvalidArgumentException
*/
public function testConstructorShouldVerifyHashAlgo()
{
new Signer('secret', 'invalid_hash_algo');
}
public function testShouldVerifyValidSignature()
{
$signer = new Signer('secret', 'sha1');
$value = 'foobar';
$signedValue = $signer->getSignedValue($value);
$this->assertNotSame($signedValue, $value);
$this->assertFalse($signer->verifySignedValue($value));
$this->assertTrue($signer->verifySignedValue($signedValue));
}
public function testShouldRejectInvalidSignature()
{
$signer = new Signer('secret', 'sha1');
$value = 'foobar';
$signedValue = $signer->getSignedValue($value, 'fake signature');
$this->assertFalse($signer->verifySignedValue($signedValue));
}
public function testSignatureShouldDependOnSecret()
{
$signer1 = new Signer('secret1', 'sha1');
$signer2 = new Signer('secret2', 'sha1');
$this->assertNotSame($signer1->getSignedValue('foobar'), $signer2->getSignedValue('foobar'));
}
}
Tests/Twig/IntegrationTest.php 0000666 00000007147 13244772066 0012444 0 ustar 00 getMockBuilder('Nelmio\SecurityBundle\ContentSecurityPolicy\ShaComputer')
->disableOriginalConstructor()
->getMock();
$shaComputer->expects($this->exactly(1))
->method('computeForScript')
->will($this->returnValue('sha-script'));
$shaComputer->expects($this->exactly(1))
->method('computeForStyle')
->will($this->returnValue('sha-style'));
$listener = $this->getMockBuilder('Nelmio\SecurityBundle\EventListener\ContentSecurityPolicyListener')
->disableOriginalConstructor()
->getMock();
$listener->expects($this->never())
->method('addSha');
$listener->expects($this->exactly(1))
->method('addScript')
->will($this->returnCallback(function ($script) use (&$collectedShas, $shaComputer) {
$collectedShas['script-src'][] = $shaComputer->computeForScript($script);
}));
$listener->expects($this->exactly(1))
->method('addStyle')
->will($this->returnCallback(function ($style) use (&$collectedShas, $shaComputer) {
$collectedShas['style-src'][] = $shaComputer->computeForStyle($style);
}));
$twig = new \Twig_Environment(new \Twig_Loader_Filesystem(__DIR__.'/templates'));
$twig->addExtension(new NelmioCSPTwigExtension($listener, $shaComputer));
$this->assertSame('
', $twig->render('test-dynamic.twig', array('api_key' => '123456', 'color' => 'black')));
$this->assertSame(array('script-src' => array('sha-script'), 'style-src' => array('sha-style')), $collectedShas);
}
public function testItWorksStatically()
{
$collectedShas = array();
$shaComputer = $this->getMockBuilder('Nelmio\SecurityBundle\ContentSecurityPolicy\ShaComputer')
->disableOriginalConstructor()
->getMock();
$shaComputer->expects($this->exactly(1))
->method('computeForScript')
->will($this->returnValue('sha-script'));
$shaComputer->expects($this->exactly(1))
->method('computeForStyle')
->will($this->returnValue('sha-style'));
$listener = $this->getMockBuilder('Nelmio\SecurityBundle\EventListener\ContentSecurityPolicyListener')
->disableOriginalConstructor()
->getMock();
$listener->expects($this->exactly(2))
->method('addSha')
->will($this->returnCallback(function ($directive, $sha) use (&$collectedShas) {
$collectedShas[$directive][] = $sha;
}));
$listener->expects($this->never())
->method('addScript');
$listener->expects($this->never())
->method('addStyle');
$twig = new \Twig_Environment(new \Twig_Loader_Filesystem(__DIR__.'/templates'));
$twig->addExtension(new NelmioCSPTwigExtension($listener, $shaComputer));
$this->assertSame('
', $twig->render('test-static.twig'));
$this->assertSame(array('script-src' => array('sha-script'), 'style-src' => array('sha-style')), $collectedShas);
}
}
Tests/Twig/templates/test-dynamic.twig 0000666 00000000313 13244772066 0014067 0 ustar 00 {% cspscript %}
{% endcspscript %}
{% cspstyle %}
{% endcspstyle %}
Tests/Twig/templates/test-static.twig 0000666 00000000273 13244772066 0013737 0 ustar 00 {% cspscript %}
{% endcspscript %}
{% cspstyle %}
{% endcspstyle %}
Tests/bootstrap.php 0000666 00000001004 13244772066 0010406 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Twig;
use Nelmio\SecurityBundle\ContentSecurityPolicy\ShaComputer;
use Nelmio\SecurityBundle\EventListener\ContentSecurityPolicyListener;
use Nelmio\SecurityBundle\Twig\TokenParser\CSPScriptParser;
use Nelmio\SecurityBundle\Twig\TokenParser\CSPStyleParser;
class NelmioCSPTwigExtension extends \Twig_Extension
{
private $listener;
private $shaComputer;
public function __construct(ContentSecurityPolicyListener $listener, ShaComputer $shaComputer)
{
$this->listener = $listener;
$this->shaComputer = $shaComputer;
}
public function getTokenParsers()
{
return array(new CSPScriptParser($this->shaComputer), new CSPStyleParser($this->shaComputer));
}
public function getListener()
{
return $this->listener;
}
public function getName()
{
return 'Nelmio\\SecurityBundle\\Twig\\NelmioCSPTwigExtension';
}
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('csp_nonce', array($this, 'getCSPNonce')),
);
}
public function getCSPNonce($usage = null)
{
if (null === $nonce = $this->listener->getNonce($usage)) {
throw new \RuntimeException('You must enable nonce to use this feature');
}
return $nonce;
}
}
Twig/Node/CSPNode.php 0000666 00000003262 13244772066 0010331 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
class CSPNode extends \Twig_Node
{
private $sha;
private $directive;
public function __construct(\Twig_Node $body, $lineno, $tag, $directive, $sha = null)
{
parent::__construct(array('body' => $body), array(), $lineno, $tag);
$this->sha = $sha;
$this->directive = $directive;
}
public function compile(\Twig_Compiler $compiler)
{
$body = $this->getNode('body');
if (null !== $this->sha) {
$output = "\$this->env->getExtension('Nelmio\SecurityBundle\Twig\NelmioCSPTwigExtension')->getListener()->addSha('{$this->directive}', '{$this->sha}');\necho ob_get_clean();\n";
} elseif ($this->directive === 'script-src') {
$output = "\$script = ob_get_clean();\n\$this->env->getExtension('Nelmio\SecurityBundle\Twig\NelmioCSPTwigExtension')->getListener()->addScript(\$script);\necho \$script;\n";
} elseif ($this->directive === 'style-src') {
$output = "\$style = ob_get_clean();\n\$this->env->getExtension('Nelmio\SecurityBundle\Twig\NelmioCSPTwigExtension')->getListener()->addStyle(\$style);\necho \$style;\n";
} else {
throw new \InvalidArgumentException(sprintf('Unable to compile for directive "%s"', $this->directive));
}
$compiler
->addDebugInfo($this)
->write("ob_start();\n")
->subcompile($body)
->write($output)
;
}
}
Twig/TokenParser/AbstractCSPParser.php 0000666 00000002750 13244772066 0013735 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Twig\TokenParser;
use Nelmio\SecurityBundle\ContentSecurityPolicy\ShaComputer;
use Nelmio\SecurityBundle\Twig\Node\CSPNode;
abstract class AbstractCSPParser extends \Twig_TokenParser
{
protected $shaComputer;
private $directive;
private $tag;
public function __construct(ShaComputer $shaComputer, $tag, $directive)
{
$this->shaComputer = $shaComputer;
$this->tag = $tag;
$this->directive = $directive;
}
public function parse(\Twig_Token $token)
{
$lineno = $token->getLine();
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideCSPScriptEnd'), true);
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
$sha = null;
if ($body instanceof \Twig_Node_Text) {
$sha = $this->computeSha($body->getAttribute('data'));
}
return new CSPNode($body, $lineno, $this->tag, $this->directive, $sha);
}
public function decideCSPScriptEnd(\Twig_Token $token)
{
return $token->test('end'.$this->tag);
}
public function getTag()
{
return $this->tag;
}
abstract protected function computeSha($data);
}
Twig/TokenParser/CSPScriptParser.php 0000666 00000001210 13244772066 0013424 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Twig\TokenParser;
use Nelmio\SecurityBundle\ContentSecurityPolicy\ShaComputer;
class CSPScriptParser extends AbstractCSPParser
{
public function __construct(ShaComputer $shaComputer)
{
parent::__construct($shaComputer, 'cspscript', 'script-src');
}
protected function computeSha($data)
{
return $this->shaComputer->computeForScript($data);
}
}
Twig/TokenParser/CSPStyleParser.php 0000666 00000001204 13244772066 0013263 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\Twig\TokenParser;
use Nelmio\SecurityBundle\ContentSecurityPolicy\ShaComputer;
class CSPStyleParser extends AbstractCSPParser
{
public function __construct(ShaComputer $shaComputer)
{
parent::__construct($shaComputer, 'cspstyle', 'style-src');
}
protected function computeSha($data)
{
return $this->shaComputer->computeForStyle($data);
}
}
UserAgent/UAFamilyParser/DoctrineCacheUAFamilyParser.php 0000666 00000002066 13244772066 0017261 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\UserAgent\UAFamilyParser;
use Doctrine\Common\Cache\Cache;
class DoctrineCacheUAFamilyParser implements UAFamilyParserInterface
{
private $cache;
private $parser;
private $lifetime;
private $prefix;
public function __construct(Cache $cache, UAFamilyParser $parser, $lifetime = 0, $prefix = 'nelmio-ua-parser-')
{
$this->parser = $parser;
$this->cache = $cache;
$this->lifetime = $lifetime;
$this->prefix = $prefix;
}
public function getUaFamily($userAgent)
{
$id = $this->prefix.md5($userAgent);
if (false !== $name = $this->cache->fetch($id)) {
return $name;
}
$name = $this->parser->getUaFamily($userAgent);
$this->cache->save($id, $name, $this->lifetime);
return $name;
}
}
UserAgent/UAFamilyParser/PsrCacheUAFamilyParser.php 0000666 00000002222 13244772066 0016250 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\UserAgent\UAFamilyParser;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Cache\CacheException;
class PsrCacheUAFamilyParser implements UAFamilyParserInterface
{
private $cache;
private $parser;
private $lifetime;
private $prefix;
public function __construct(CacheItemPoolInterface $cache, UAFamilyParser $parser, $lifetime = 0, $prefix = 'nelmio-ua-parser-')
{
$this->parser = $parser;
$this->cache = $cache;
$this->lifetime = $lifetime;
$this->prefix = $prefix;
}
public function getUaFamily($userAgent)
{
$id = $this->prefix.md5($userAgent);
$item = $this->cache->getItem($id);
if ($item->isHit()) {
return $item->get();
}
$name = $this->parser->getUaFamily($userAgent);
$this->cache->save($item->set($name)->expiresAfter($this->lifetime));
return $name;
}
}
UserAgent/UAFamilyParser/UAFamilyParser.php 0000666 00000001147 13244772066 0014644 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\UserAgent\UAFamilyParser;
use UAParser\Parser;
class UAFamilyParser implements UAFamilyParserInterface
{
private $parser;
public function __construct(Parser $parser)
{
$this->parser = $parser;
}
public function getUaFamily($userAgent)
{
return strtolower($this->parser->parse($userAgent)->ua->family);
}
}
UserAgent/UAFamilyParser/UAFamilyParserInterface.php 0000666 00000000563 13244772066 0016466 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\UserAgent\UAFamilyParser;
interface UAFamilyParserInterface
{
public function getUaFamily($userAgent);
}
UserAgent/UserAgentParser.php 0000666 00000002664 13244772066 0012253 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\UserAgent;
use Nelmio\SecurityBundle\UserAgent\UAFamilyParser\UAFamilyParserInterface;
class UserAgentParser implements UserAgentParserInterface
{
private $parser;
public function __construct(UAFamilyParserInterface $parser)
{
$this->parser = $parser;
}
/**
* {@inheritdoc}
*/
public function getBrowser($userAgent)
{
$name = $this->parser->getUaFamily($userAgent);
switch (true) {
case 'chrome' === $name:
return self::BROWSER_CHROME;
case 'firefox' === $name:
return self::BROWSER_FIREFOX;
case 'opera' === $name:
return self::BROWSER_OPERA;
case 'safari' === $name:
case 'mobile safari' === $name:
return self::BROWSER_SAFARI;
case 'chrome ' === substr($name, 0, 7):
return self::BROWSER_CHROME;
case 'firefox ' === substr($name, 0, 8):
return self::BROWSER_FIREFOX;
case 'opera ' === substr($name, 0, 6):
return self::BROWSER_OPERA;
default:
return self::BROWSER_OTHER;
}
}
}
UserAgent/UserAgentParserInterface.php 0000666 00000001272 13244772066 0014066 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\SecurityBundle\UserAgent;
interface UserAgentParserInterface
{
const BROWSER_CHROME = 'Chrome';
const BROWSER_OPERA = 'Opera';
const BROWSER_FIREFOX = 'Firefox';
const BROWSER_SAFARI = 'Safari';
const BROWSER_OTHER = 'Other';
/**
* @param $userAgent string The user agent to parse
*
* @return string One of the UserAgentParserInterface::BROWSER_* constant
*/
public function getBrowser($userAgent);
}
composer.json 0000666 00000002316 13244772066 0007307 0 ustar 00 {
"name": "nelmio/security-bundle",
"description": "Extra security-related features for Symfony: signed/encrypted cookies, HTTPS/SSL/HSTS handling, cookie session storage, ...",
"keywords": ["security"],
"type": "symfony-bundle",
"license": "MIT",
"authors": [
{
"name": "Nelmio",
"homepage": "http://nelm.io"
},
{
"name": "Symfony Community",
"homepage": "https://github.com/nelmio/NelmioSecurityBundle/contributors"
}
],
"require": {
"paragonie/random_compat": "~1.0|~2.0",
"symfony/framework-bundle": "~2.3|~3.0|~4.0",
"symfony/security": "~2.3|~3.0|~4.0",
"ua-parser/uap-php": "^3.4.4"
},
"require-dev": {
"doctrine/cache": "^1.0",
"psr/cache": "^1.0",
"twig/twig": "^1.24",
"symfony/yaml": "~2.3|~3.0|~4.0",
"symfony/phpunit-bridge": "^3.2|~4.0"
},
"suggest": {
"ua-parser/uap-php": "To allow adapt CSP directives given the user-agent"
},
"autoload": {
"psr-4": { "Nelmio\\SecurityBundle\\": "" }
},
"extra": {
"branch-alias": {
"dev-master": "2.4.x-dev"
}
}
}
phpunit.xml.dist 0000666 00000001524 13244772066 0007740 0 ustar 00
./Tests/
./
./Resources
./Tests/bootstrap.php