A Julia library to aid the integration of Content-Security-Policy headers into web applications.
References
The package is under active development and changes may occur.
- 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
All are welcome, as well as feature requests and bug reports. Please open an issue, discussion topic or submit a PR.
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
using ContentSecurityPolicy
Can be used as CSP
, for name shortening purposes
using ContentSecurityPolicy
CSP.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.
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 # "*"
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"
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'">
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"
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 = 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"
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"