Skip to content

charlieIT/ContentSecurityPolicy.jl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

35 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ContentSecurityPolicy

A Julia library to aid the integration of Content-Security-Policy headers into web applications.

References

Project status

The package is under active development and changes may occur.

ToDo

  • Register package
  • Improve support for csp-nonce and csp-hash
  • Improve default strict policy and improve overall configurability
  • Handle CSP violation reports
  • Export nginx and Apache header configurations

Contributions, suggestions, questions

All are welcome, as well as feature requests and bug reports. Please open an issue, discussion topic or submit a PR.

Table of Contents

  1. Installation
  2. Usage examples
  3. Web example
  4. Import from JSON
  5. API Reference

Installation

The package can be installed via package manager

pkg> add ContentSecurityPolicy

It can also be installed by providing a URL to the repository

pkg> add https://github.com/charlieIT/ContentSecurityPolicy.jl

Usage examples

using ContentSecurityPolicy

Can be used as CSP, for name shortening purposes

using ContentSecurityPolicy
CSP.Policy()

Build a Content Security Policy

Policy(
   # Set fallback for all fetch directives
    "default-src"=>"*",
    # Set valid sources of images and favicons
    "img-src"=>("'self'", "data:"),
    # Turn on https enforcement
    "upgrade-insecure-requests"=>true,
    # Custom directives are supported, if needed
    "some-custom-directive"=>["foo", "bar"]
)
Output
{
    "default-src": "*",
    "img-src": [
        "'self'",
        "data:"
    ],
    "upgrade-insecure-requests": true,
    "some-custom-directive": [
        "foo",
        "bar"
    ],
    "report-only": false
}

See also: Policy, Strict Policy.

Edit existing policy

Modify multiple directives at once

# Modify multiple directives at once
policy(
    # Pairs before kwargs
    "script-src" => ("'unsafe-inline'", "http://example.com"),
    img_src = ("'self'", "data:")
)

Modify single directive

# Modify individually via directive name
policy["img-src"] = CSP.wildcard # "*"

Build http headers

Content-Security-Policy header

using ContentSecurityPolicy, HTTP

HTTP.Header(Policy(default=true))
"Content-Security-Policy" => "base-uri none; default-src 'self'; frame-ancestors none; object-src none; report-to default; script-src 'strict-dynamic'"

Report-Only header

policy = Policy(
        "default-src"=>CSP.self,
        "report-to"=>"some-endpoint",
        report_only=true)
        
HTTP.Header(policy)
"Content-Security-Policy-Report-Only" => "default-src 'self'; report-to some-endpoint"

Build <meta> element

Construction will automatically ignore directives that are not supported in the <meta> element. Currently [frame-ancestors, report-uri, report-to, sandbox].

See also mdn csp directives.

CSP.meta(Policy(report_to="default", default_src="'self'"))
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">

Obtain CSP header as Dict

policy = csp("default-src"=>CSP.self, "img-src"=>(CSP.self, CSP.data), "report-uri"=>"/api/reports")

CSP.http(policy)
Output
OrderedCollections.OrderedDict{String, Any} with 3 entries:
  "img-src"     => "data: 'self'"
  "default-src" => "'self'"
  "report-uri"  => "/api/reports"

Web example

Mockup web application with dynamic CSP policies, that can also receive CSP violation reports.

The example app will allow route handlers to tailor the CSP Policy on each response.

using ContentSecurityPolicy, Dates, HTTP, JSON3, Random, Sockets

Middleware for adding CSP header to each response

"""
A middleware that will set a restrictive default policy.

Allows route handlers to change the CSP Policy
"""
function CSPMiddleware(next)
    return function(request::HTTP.Request)

        function respond(response::HTTP.Response)
            timestamp = string(round(Int, datetime2unix(now())))
                
            # A default restrictive policy
            policy = csp(
                default = true, 
                default_src = "'self'", 
                script_src = "none",
                report_to = false,
                sandbox = true, 
                report_uri = "/reports/$timestamp") # report to specific endpoint

            if !isnothing(request.context)
                if haskey(request.context, :csp)
                    # Acquire the policy defined by the route and log
                    route_policy = request.context[:csp]
                    @info "Custom policy: $(string(route_policy))"

                    # Merge default with handler provided policy
                    policy = policy(route_policy.directives...)
                end 
            end
            # Check whether header was not yet defined
            if !HTTP.hasheader(response, CSP.CSP_HEADER)
                # Set CSP policy header
                HTTP.setheader(response, HTTP.Header(policy))
            end
            return response
        end
	return respond(next(request))
    end
end

Handler for posted csp violation reports

"""
Handle posted CSP Reports
"""
function report(request::HTTP.Request)
    report = String(request.body)
    # Each report is posted to /reports/{timestamp}
    timestamp = Base.parse(Int, request.context[:params]["timestamp"])
    # Log timestamp as Date
    println(string("Timestamp: ", unix2datetime(timestamp)))
    # Log pretty json report
    JSON3.pretty(report)

    return HTTP.Response(200, report)
end

A page with restrictive csp policy

function restrictive(request::HTTP.Request)
    # Obtain a nonce
    nonce = CSP.csp_nonce()
    # Set a policy allowing scripts with our nonce, also enabling scripts and modals in sandbox mode
    request.context[:csp] = csp(script_src="'nonce-$nonce'", sandbox="allow-scripts allow-modals")

    html = """
    <html>
        <body>
            <!-- This will execute -->
            <script type="text/javascript", nonce='$nonce'>
                alert('I can execute!');
            </script>
            
            <!-- This should not execute -->
            <script type="text/javascript">
                alert('Not authorised!');
            </script>
        </body>
    </html>
    """
    return HTTP.Response(200, html)
end

A page with a more permissive csp policy

function permissive(request::HTTP.Request)
    # Set permissive script-src to allow all inline scripts
    request.context[:csp] = csp("script-src"=>("'self'", "'unsafe-inline'"), "sandbox"=>false)

    html = """
    <html>
        <body>
            <div id="hello"></div>
            <script type="text/javascript">
                document.getElementById('hello').innerHTML = 'Scripts can execute!';
            </script>
            <script type="text/javascript">
                alert('Scripts can launch modals!');
            </script>
        </body>
    </html>
    """
    return HTTP.Response(200, html)
end

Setup http routing

const csp_router = HTTP.Router()
HTTP.register!(csp_router, "GET", "/restrictive", restrictive)
HTTP.register!(csp_router, "GET", "/permissive", permissive)
# Handle incoming CSP reports
HTTP.register!(csp_router, "POST", "/reports/{timestamp}", report)

server = HTTP.serve!(csp_router |> CSPMiddleware, ip"0.0.0.0", 80)

See also: web example.

Policy from a JSON file

Example configuration.json

policy = Policy("/path/to/conf.json")
policy["default-src"]
Output
8-element Vector{String}:
 "'unsafe-eval'"
 "'unsafe-inline'"
 "data:"
 "filesystem:"
 "about:"
 "blob:"
 "ws:"
 "wss:"
julia> policy["script-src"]
Output
3-element Vector{String}:
 "'unsafe-eval'"
 "'unsafe-inline'"
 "https://www.google-analytics.com"

API Reference

Strict Policy

DEFAULT_POLICY

Work in progress. A default, restrictive policy based on various CSP recommendations. Used when creating a Policy where default = true.

See also: OWASP CSP cheatsheet, mdn csp docs, csp.withgoogle.com, CSP Is Dead, Long Live CSP! and strict-csp.


const DirectiveTypes = Union{String, Set{String}, Vector{String}, Tuple, Bool}

Defines acceptable values of a directive.

Empty and false values are not considered when generating a CSP header.


Policy(directives::AbstractDict, report_only=false)
Parameter Type Description
directives Dict{String, DirectiveTypes} Set of directives that configure your policy
report_only Bool Optional Whether to define Policy as report only. Defaults to false

Default constructor. Policies are empty by default.

julia> Policy()
{
    "report-only": false
}

Policy(directives::Pair...; default=false, report_only=false, kwargs...)
Parameter Type Description
directives Pair{String,DirectiveTypes} Individual policies as a Pair.
default Bool Optional Whether to add default directives and default values. Defaults to false
report_only Bool Optional Whether to define Policy as report only. Defaults to false
kwargs Directives Optional Directives as keyword arguments. Automatically replaces _ with - in known directives.
Examples
Policy("script-src"=>"https://example.com/", "img-src"=>"*", report_only=true)
{
    "img-src": "*",
    "script-src": "https://example.com/",
    "report-only": true
}
policy = Policy(
     # Set default-src
     default_src = CSP.self, # "'self'"
     # Set report-uri
     report_uri = "https://example.com",
     # Report endpoint
     report_to = "default",
     sandbox = "allow-downloads",
     # Turn on https enforcement
     upgrade_insecure_requests = true)
{
    "upgrade-insecure-requests": true,
    "default-src": "'self'",
    "report-to": "default",
    "sandbox": "allow-downloads",
    "report-uri": "https://example.com",
    "report-only": false
}

Policy(json::String)
Parameter Type Description
json String Path to json file, or json string

Build a Policy from a JSON configuration.

See also: Import from JSON


HTTP.Header(policy::Policy)
Parameter Type Description
policy Policy A Policy instance

Build CSP Header

Example
HTTP.Header(Policy(default=true))
"Content-Security-Policy" => "base-uri none; default-src 'self'; frame-ancestors none; object-src none; report-to default; script-src 'strict-dynamic'"

CSP.meta(policy::Policy; except=CSP.META_EXCLUDED)
Parameter Type Description
policy Policy A Policy instance
except Vector{String} Optional Set of directives to exclude from meta element. Defaults to CSP.META_EXCLUDED

Build <meta> element, ignoring directives in except

Example
CSP.meta(Policy(report_to="default", default_src="'self'"))
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">

CSP.http(policy::Policy)
Parameter Type Description
policy Policy A Policy instance

Obtain CSP headers as Dict

Example
policy = csp("default-src"=>CSP.self, "img-src"=>(CSP.self, CSP.data), "report-uri"=>"/api/reports")

CSP.http(policy)
OrderedCollections.OrderedDict{String, Any} with 3 entries:
  "img-src"     => "data: 'self'"
  "default-src" => "'self'"
  "report-uri"  => "/api/reports"