Code Monkey home page Code Monkey logo

kirby3-security-headers's Introduction

Kirby Content Security Policy Header

Release Downloads Build Status Coverage Status Maintainability
Twitter

Kirby Plugin for easier Security Headers setup.

🔐 Why should you use this plugin? Because security matters. Protecting your own or your clients websites and their customers data is important.

  1. Automatic Setup
  2. Setup: Headers
  3. Setup: Loader
  4. Setup: Setter
  5. Frontend Nonce
  6. Settings

Commercial Usage


Support open source!

This plugin is free but if you use it in a commercial project please consider to sponsor me or make a donation.
If my work helped you to make some cash it seems fair to me that I might get a little reward as well, right?

Be kind. Share a little. Thanks.

‐ Bruno
 
M O N E Y
Github sponsor Patreon Buy Me a Coffee Paypal dontation Hire me

Installation

  • unzip master.zip as folder site/plugins/kirby3-security-headers or
  • git submodule add https://github.com/bnomei/kirby3-security-headers.git site/plugins/kirby3-security-headers or
  • composer require bnomei/kirby3-security-headers

Setup

Automatic

A route:before-hook takes care of setting the headers automatically unless one of the following conditions applies:

  • Kirbys global debug mode is true
  • Kirby determins it is a local setup
  • the plugins setting enabled is set to false

Header

The following headers will be applied by default. You can override them in the config file.

/site/config/config.php

<?php
return [
    'bnomei.securityheaders.headers' => [
        "X-Powered-By" => "", // unset
        "X-Frame-Options" => "SAMEORIGIN",
        "X-XSS-Protection" => "1; mode=block",
        "X-Content-Type-Options" => "nosniff",
        "strict-transport-security" => "max-age=31536000; includeSubdomains",
        "Referrer-Policy" => "no-referrer-when-downgrade",
        "Permissions-Policy" => 'interest-cohort=()', // flock-off,
        // ... FEATURE POLICIES
    // other options...
];

Loader

The Loader is used to initally create the CSPBuilder object with a given set of data. You skip that, forward a file to load, provide an array or use the default loader file. Using a custom file is recommended when for example adding additional font-src for google web fonts.

/site/config/config.php

<?php
return [
    'bnomei.securityheaders.loader' => function () {
        // https://github.com/paragonie/csp-builder#example
        // null if you do NOT want to use default and/or just the setter
        /*
            return null;
         */
        // return path of file (json or yaml)
        // or an array of options for the cspbuilder
        /*
            return [...];
            return kirby()->roots()->site() . '/your-csp.json';
            return kirby()->roots()->site() . '/your-csp.yml';
        */
        // otherwise forward the default file from this plugin
        return __DIR__ . '/loader.json';
    },
    // other options...
];

Setter

The Setter is applied after the Loader. Use it to add dynamic stuff like hashes and nonces.

/site/config/config.php

<?php
return [
    'bnomei.securityheaders.setter' => function (\Bnomei\SecurityHeaders $instance) {
        // https://github.com/paragonie/csp-builder#build-a-content-security-policy-programmatically
        /** @var ParagonIE\CSPBuilder\CSPBuilder $csp */
        /*
            $csp = $instance->csp();
            $nonce = $instance->setNonce('my-inline-script');
            $csp->nonce('script-src', $nonce);
        */
        // in your template retrieve it again with
        /*
            $nonce = $page->nonce('my-inline-script');
            => `THIS-IS-THE-NONCE`
            $attr = $page->nonceAttr('my-inline-script');
            => `nonce="THIS-IS-THE-NONCE"`
        */
    },
    // other options...
];

TIP: nonces are set in the setter and later retrieved using $page->nonce(...) or $page->nonceAttr(...).

Panel and Frontend Nonces

This plugin automatically registers Kirbys nonce for the panel. For convenience it also provides you with a single frontend nonce to use as attribute in <link>, <style> and <script> elements. You can retrieve the nonce with site()->nonce() and the full attribute with site()->nonceAttr().

<?php ?>

<script nonce="<?= site()->nonce() ?>">
// ...
</script>

<style <?= site()->nonceAttr() ?>>

</style>

TIP: The srcset plugin uses that frontend nonce as well.

Settings

bnomei.securityheaders. Default Description
enabled true or false or 'force' will set headers
seed callback returns a seed for frontend nonce
headers array of sensible default values. modify as needed.
loader callback returning filepath or array
setter callback instance which allows customizing the CSPBuilder

Dependencies

Disclaimer

This plugin is provided "as is" with no guarantee. Use it at your own risk and always test it yourself before using it in a production environment. If you find any issues, please create a new issue.

License

MIT

It is discouraged to use this plugin in any project that promotes racism, sexism, homophobia, animal abuse, violence or any other form of hate speech.

kirby3-security-headers's People

Contributors

bnomei avatar yoanmalie avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

wout amishakov

kirby3-security-headers's Issues

Disable in panel

Hey there,
I know that you struggled with this in #11, but there's an edge case I cannot solve for now:

When using your plugin together with custom-add-fields, the latter stops working. Since I can't figure out how to fix this, I could circumvent this problem by disabling your plugin on the panel.

I'd be happy to provide a PR with an option like bnomei.securityheaders.enablePanel or something like this ..

Cheers!

PS: If you're able to spot the problem in @steirico's index.js with the speed of lightning, feel free to let us know 🦊

<style> doesn't respect $site->nonce()

Hey there,
when playing around with your plugin, I noticed that $site->nonce() works for CSS files (via <link> tag) and JS files (via <script> tag), but not for inline CSS via <style> - which is odd ..

My code looks like this:

<?php if (!option('debug')) : ?>
<?php
    # Production = Minified inline CSS
    $cssPath = $kirby->root('assets') . '/styles/main.min.css';
    $css = F::read($cssPath);
?>
<style nonce="<?= $site->nonce() ?>"><?= $css ?></style>

<?php else : ?>

<?php
    # Development = Unminified CSS file
    $cssPath = '/assets/styles/main.css';
    $css = Bnomei\Fingerprint::css($cssPath, [
        'nonce' => $site->nonce(),
        'integrity' => true,
    ]);

    echo $css;
?>
<?php endif ?>

<?php
    $jsFile = option('debug') ? 'main.js' : 'main.min.js';
    $jsPath = '/assets/scripts/' . $jsFile;

    echo Bnomei\Fingerprint::js($jsPath, [
        'nonce' => $site->nonce(),
        'defer' => true,
        'integrity' => true
    ]);
?>

The only part failing me is <style nonce="<?= $site->nonce() ?>"><?= $css ?></style> - which I cannot explain ..

Fix nonce generation

When generating a nonce using the setNonce() method, a random string with the prefix 'nonce-' is generated. Unfortunately this prefix doesn't appear correctly in the CSP. The '-' is missing.

Example:
Nonce in JS attribute: nonce-ZGJhOWRiYzM0YzM3NjZkOTNhNDI2OGFkMjczMDRkNjczYzA3MDhjNA==
Nonce in CSP header: nonce-nonceZGJhOWRiYzM0YzM3NjZkOTNhNDI2OGFkMjczMDRkNjczYzA3MDhjNA==

By just removing the '-' from the prefix in setNonce() the issue is gone.

[FAQ] Which policies should I create?

Question:
Which policies should I create?

Answer:
The default values for this plugin are a good start and in most cases you just need to define some additional policies. Just make sure not to weaken the policies by enabling unsafe-inline etc. Try finding the secure way to do these things.

  1. Record what you use: https://addons.mozilla.org/en-US/firefox/addon/laboratory-by-mozilla/
  2. Generate full list: https://www.cspisawesome.com/
  3. Set it up with this plugin
  4. Validate if it works: http://securityheaders.com/

nonce attribute is empty if config.php : debug => false

Using <?= $site->nonceAttr() ?> returns an empty attribute, but if printed out in, for example, a paragraphs inner HTML it is not empty. I disabled or removed any plugin manipulating the html code but the problem persists.
If the debug mode in the config.php is enabled, everything works as expected but that is not a solution.

I am using this plugin in Kirby 4.

Make it possible to enable CSP in debug mode

I'd like to have this plugin enabled during development to not only spot possible breakage through CSP issues in production. But currently setting 'debug' => true always disables it.

I have two ideas how to solve this:

As a temporary workaround I use this code/configuration for my dev setup:

return [
    'debug' => true,
    /**
     * Weird workaround to enable the plugin even if debug is true
     * Works like this:
     * 1. Set enabled to false to skip the plugin instantiating the singleton
     * 2. Instantiate it on our own but forcing debug to false and enabled to true
     * 3. Profit
     */
    'bnomei.securityheaders.enabled' => false,
    'hooks' => [
        'route:before' => function (): void {
            \Bnomei\SecurityHeaders::singleton([
                'debug' => false,
                'enabled' => true,
            ])->sendHeaders();
        },
    ],
];

Configured CSP header is ignored

Kirby v3, security headers plugin and dependencies installed via composer in the most recent versions by the time of writing.

I set up this plugin to output CSP headers matching required hostnames from the website like this:

'bnomei.securityheaders.headers' => [
        "X-Powered-By" => "", // unset
        "X-Frame-Options" => "SAMEORIGIN",
        "X-XSS-Protection" => "1; mode=block",
        "X-Content-Type-Options" => "nosniff",
        "strict-transport-security" => "max-age=31536000; includeSubdomains",
        "Referrer-Policy" => "no-referrer-when-downgrade",
        // Generated with https://www.cspisawesome.com/content_security_policies
        "Content-Security-Policy" => "default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' stats.autark-app.de www.google.com www.gstatic.com; style-src 'self' 'unsafe-inline' fonts.googleapis.com; img-src 'self' data: a.basemaps.cartocdn.com b.basemaps.cartocdn.com c.basemaps.cartocdn.com; frame-src www.google.com; font-src 'self' fonts.gstatic.com; connect-src 'self'",
        "X-Content-Security-Policy" => "default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' stats.autark-app.de www.google.com www.gstatic.com; style-src 'self' 'unsafe-inline' fonts.googleapis.com; img-src 'self' data: a.basemaps.cartocdn.com b.basemaps.cartocdn.com c.basemaps.cartocdn.com; frame-src www.google.com; font-src 'self' fonts.gstatic.com; connect-src 'self'",
        "X-WebKit-CSP" => "default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' stats.autark-app.de www.google.com www.gstatic.com; style-src 'self' 'unsafe-inline' fonts.googleapis.com; img-src 'self' data: a.basemaps.cartocdn.com b.basemaps.cartocdn.com c.basemaps.cartocdn.com; frame-src www.google.com; font-src 'self' fonts.gstatic.com; connect-src 'self'",
    ],

But it seems to be ignored, as the headers sent to the browser are these:

Content-Security-Policy: base-uri 'self'; default-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; frame-src; img-src 'self' data:; media-src 'none'; object-src 'none'; script-src 'self' 'nonce-MTc1NWZjOGY1NDRiNmE0MmI3MjM1ZGUxNTdiZmI3MjU5YzgxNDMwYg=='; style-src 'self' 'nonce-MTc1NWZjOGY1NDRiNmE0MmI3MjM1ZGUxNTdiZmI3MjU5YzgxNDMwYg=='; worker-src; upgrade-insecure-requests

[FAQ] How to allow Vimeo or Youtube

Question:
How to add directives for other domains?

Answer:
you could create a custom snippet based on default one or just override the csp in your config file. Example for vimeo:

<?php
use Phpcsp\Security\ContentSecurityPolicyHeaderBuilder;

return [
   'bnomei.securityheaders.csp' => function() {
        $policy = new ContentSecurityPolicyHeaderBuilder();

        // root domain
        $sourcesetID = kirby()->site()->title()->value();
        $policy->defineSourceSet($sourcesetID, [kirby()->site()->url()]);

        $directives = [
            ContentSecurityPolicyHeaderBuilder::DIRECTIVE_DEFAULT_SRC,
            ContentSecurityPolicyHeaderBuilder::DIRECTIVE_STYLE_SRC,
            ContentSecurityPolicyHeaderBuilder::DIRECTIVE_SCRIPT_SRC,
            ContentSecurityPolicyHeaderBuilder::DIRECTIVE_IMG_SRC,
            ContentSecurityPolicyHeaderBuilder::DIRECTIVE_FONT_SRC,
            ContentSecurityPolicyHeaderBuilder::DIRECTIVE_CONNECT_SRC,
        ];
        foreach ($directives as $d) {
            $policy->addSourceSet($d, $sourcesetID);
        }

        // vimeo
        $sourcesetID = 'vimeo';
        $policy->defineSourceSet($sourcesetID, ['player.vimeo.com']);

        $directives = [
            ContentSecurityPolicyHeaderBuilder::DIRECTIVE_DEFAULT_SRC,
            ContentSecurityPolicyHeaderBuilder::DIRECTIVE_STYLE_SRC,
            ContentSecurityPolicyHeaderBuilder::DIRECTIVE_SCRIPT_SRC,
            ContentSecurityPolicyHeaderBuilder::DIRECTIVE_IMG_SRC,
        ];
        foreach ($directives as $d) {
            $policy->addSourceSet($d, $sourcesetID);
        }

        return $policy;
    },
  ];

Error when trying to implement a hash

I have a little inline style applied to the body tag (style="height: 100%;") that I need to use a hash for in my CSP.

I have added this to my .json file used to generate my CSP, but am getting an error when loading the page. This is my .json file in full:

{
  "report-only": false,
  "base-uri": {
    "self": true
  },
  "default-src": {
    "self": true
  },
  "connect-src": {
    "self": true
  },
  "font-src": {
    "self": true,
    "allow": ["https://use.typekit.net"]
  },
  "form-action": {
    "allow": [],
    "self": true
  },
  "frame-ancestors": [],
  "frame-src": {
    "allow": [],
    "self": false
  },
  "img-src": {
    "self": true,
    "data": true
  },
  "media-src": [],
  "object-src": [],
  "plugin-types": [],
  "script-src": {
    "allow": [],
    "hashes": [],
    "self": true,
    "unsafe-inline": true,
    "unsafe-eval": true
  },
  "style-src": {
    "self": true,
    "allow": ["https://use.typekit.net", "https://p.typekit.net"],
    "hashes": ["sha256-YTEza4CA2qPCNGLfB6mKa5FjY8kjkO/K7nQxeJxVd9E="]
  },
  "upgrade-insecure-requests": true,
  "worker-src": {
    "allow": [],
    "self": false
  }
}

As you can see I have added the hash to my style-src. The error is:

Invalid argument supplied for foreach()

On line 882 of /vendor/paragonie/csp-builder/src/CSPBuilder.php. The line in question looks like this:

foreach ($hash as $algo => $hashval) {

If you have any pointers as to where I might be going wrong it would be greatly appreciated

"nonce" question

Hello there

This plugin automatically adds nonce to headers. Is it possible to disable nonce in headers using csp loader?

'nonce-'
An allow-list for specific inline scripts using a cryptographic nonce (number used once). The server must generate a unique nonce value each time it transmits a policy. It is critical to provide an unguessable nonce, as bypassing a resource’s policy is otherwise trivial. See unsafe inline script for an example. Specifying nonce makes a modern browser ignore 'unsafe-inline' which could still be set for older browsers without nonce support.

Looks like 'unsafe-inline' doesn't work when nonce are present in headers. So without disabling nonce in headers 'unsafe-inline' option is not usable.

I know it's not a good idea to enable unsafe-inline, but sometimes inline scripts are generated by third parties.

Thank you

[FAQ] Inline Style-attribute rejected

Question
With default setup my style attributes are rejected. how to fix this?

Answer 1
Create a custom snippet and allow unsafe inline. Not a very good idea.

Answer 2
Use CSS Object Model (CSSOM) to set style. Do this by storing at adata-attribute and apply with jQuery/Zepto.
https://stackoverflow.com/questions/24713440/banned-inline-style-csp-and-dynamic-positioning-of-html-elements/29089970#29089970

json string in data attr
NOTE: json string needs " so wrap in '.

<div id="special" data-style='{ "backgroundColor": "#8EE", "fontSize": 28 }'>...</div>

jquery/zepto
https://api.jquery.com/css/#css-properties
https://zeptojs.com/#css

var specialStyle = JSON.parse($('#special').attr('data-style'));
elem.css(specialStyle);
$('[data-style]').each(function (idx, ele) {
  let data = $(ele).attr('data-style')
  if (data !== undefined && data.length > 0) {
    let style = JSON.parse(data)
    $(ele).css(style)
  }
})

Setup with custom json file

Hi,
I have this plugin working with the default setup, but am struggling to use a custom json file to adjust the policy. This is my config.php (included in full in case there is an issue elsewhere):

<?php

  return [

    'bnomei.securityheaders.loader' => function () {
        return kirby()->roots()->site() . '/csp.json';
    },

    'routes' => [
      [
        'pattern' => 'sitemap.xml',
        'action'  => function() {
            $pages = site()->pages()->index();
  
            // fetch the pages to ignore from the config settings,
            // if nothing is set, we ignore the error page
            $ignore = kirby()->option('sitemap.ignore', ['error']);
  
            $content = snippet('sitemap', compact('pages', 'ignore'), true);
  
            // return response with correct header type
            return new Kirby\Cms\Response($content, 'application/xml');
        }
      ],
      [
        'pattern' => 'sitemap',
        'action'  => function() {
          return go('sitemap.xml', 301);
        }
      ]
    ],
  ];

?>

Then in my site root I have the following json file (csp.json):

{
  "report-only": false,
  "base-uri": {
    "self": true
  },
  "default-src": {
    "self": true
  },
  "connect-src": {
    "self": true
  },
  "font-src": {
    "self": true
  },
  "form-action": {
    "allow": [],
    "self": true
  },
  "frame-ancestors": [],
  "frame-src": {
    "allow": [],
    "self": false
  },
  "img-src": {
    "self": true,
    "data": true
  },
  "media-src": [],
  "object-src": [],
  "plugin-types": [],
  "script-src": {
    "allow": [],
    "hashes": [],
    "self": true,
    "unsafe-inline": false,
    "unsafe-eval": true
  },
  "style-src": {
    "self": true
  },
  "upgrade-insecure-requests": true,
  "worker-src": {
    "allow": [],
    "self": false
  }
}

When using this setup I get no CSP generated at all. Can you please advise where I am going wrong? Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.