Nothing
#' Plugin for setting security related headers
#'
#' @description
#' This plugin is inspired by [Helmet.js](https://helmetjs.github.io/) and aids
#' you in setting response headers relevant for security of your fiery server.
#' All defaults are taken from Helmet.js as well, except for the `max-age` of the
#' `Strict-Transport-Security` header that has been doubled to 2 years which is
#' the recommendation.
#'
#' @details
#' Web security is a complicated subject and it is impossible for this document
#' to stay current and true at all times as well as be able to learn the user of
#' all the intricacies of web security. It is **strongly** advised that you
#' familiarise yourself with this subject if you plan on exposing a fiery
#' webserver to the public. A good starting point is
#' [MDN's guide on web security](https://developer.mozilla.org/docs/Web/Security).
#'
#' This plugin concerns 14 different headers that are in one way or another
#' implicated in security. Some of them are only relevant if you serve HTML
#' content on the web and have no effect on e.g. a server providing a REST api.
#' These have been marked with **UI** below. While you may turn these off for a
#' pure API server (by setting them to `NULL`), it is advised that you only
#' steer away from the defaults if you have a good grasp of the implications.
#' The headers are set very efficiently so removing some unneeded ones will only
#' have an effect on the size of the response, not the handling time.
#'
#' ## Headers
#' ### `Content-Security-Policy` (**UI**)
#' This header provides finely grained control over what code can be executed on
#' the site you provide and thus help in preventing cross-site scripting (XSS)
#' attacks. The configuration of this header is complicated and you can read
#' more about it at [the header reference](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Security-Policy)
#' and [the CSP section of the security guide](https://developer.mozilla.org/docs/Web/Security/Practical_implementation_guides/CSP)
#'
#' The plugin does some light validation of the data structure you provide and
#' you can use the [csp()] constructure to get argument tab-completion.
#'
#' ### `Content-Security-Policy-Report-Only` (**UI**)
#' This header is like `Content-Security-Policy` above except that it doesn't
#' enforce the policy but rather report any violations to a URL of your choice.
#' The reason for providing this is that setting up CSP correctly can be
#' difficult and may lead to your site not working correctly. Therefore, if you
#' apply CSP to an already excisting site it is often a good idea to start with
#' using this header and monitor where issues may arise before turning on the
#' policy fully. You provide the URL to send violation reports to with the
#' `report_to` directive which should be set to a URL. You can find more
#' information on this header at
#' [the header reference](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Security-Policy-Report-Only)
#'
#' ### `Cross-Origin-Embedder-Policy` (**UI**)
#' This header controls which resources can be embedded in a document. If set
#' to e.g. `require-corp` then only resources that implements CORP or CORS can
#' be embedded. It is not set by default in SecurityHeaders. Read more about this
#' header at [MDN](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy)
#'
#' ### `Cross-Origin-Opener-Policy` (**UI**)
#' This header controls and restricts access from cross-origin windows opened
#' from the site. It helps isolate new documents and prevent a type of attack known
#' as XS-Leaks. Read more about this header at [MDN](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy)
#' and about XS-Leaks [in the security guide](https://developer.mozilla.org/docs/Web/Security/Attacks/XS-Leaks)
#'
#' ### `Cross-Origin-Resource-Policy`
#' This header controls where the given response can be used. If you e.g. return
#' an image along with `Cross-Origin-Resource-Policy: same-site`, then this
#' image is blocked from being loaded by other sites.
#' Read more about this header at [MDN](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Cross-Origin-Resource-Policy)
#' and about CORP in general [in the security guide](https://developer.mozilla.org/docs/Web/Security/Practical_implementation_guides/CORP)
#'
#' ### `Origin-Agent-Cluster` (**UI**)
#' This header helps isolate documents served from the same site into separate
#' processes. This can improve performance of other tabs if a resource
#' intensive tab is opened but also prevent certain information from being
#' available to code running in the tab. Read more about this header at
#' [MDN](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Origin-Agent-Cluster)
#'
#' ### `Referrer-Policy` (**UI**)
#' This header instructs what to include in the `Referer` header when navigating
#' away from the document. This can potentially lead to information leakage
#' which can be alleviated using this header. Read more about this header at
#' [MDN](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Referrer-Policy)
#' as well as the [security implications of the `Referer` header](https://developer.mozilla.org/docs/Web/Security/Referer_header:_privacy_and_security_concerns)
#'
#' ### `Strict-Transport-Security`
#' This header informs a browser that the given resource should only be
#' accessed using HTTPS. This preference is cached by the browser and the next
#' time the resource is accessed over HTTP it is automatically changed to HTTPS
#' before the request is made. This header should only be sent over HTTPS to
#' prevent a manipulator-in-the-middle from alterning its settings. In order for
#' this to happen SecurityHeaders will automatically redirect any HTTP requests to
#' HTTPS if this header is set. Read more about this header at
#' [MDN](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security)
#'
#' ### `X-Content-Type-Options`
#' This header instruct the client that the MIME type provided by the
#' `Content-Type` should be respected and mime-type sniffing avoided. Setting
#' this can help prevent certain XSS attacks. Read more about this header at
#' [MDN](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options)
#' and about its security implication in the
#' [security guide](https://developer.mozilla.org/docs/Web/Security/Practical_implementation_guides/MIME_types)
#'
#' ### `X-DNS-Prefetch-Control` (**UI**)
#' This header controls DNS prefetching and domain name resolution. A browser
#' may do this in the background when a site is loaded which can reduce latency
#' when a user clicks a link. However, it may also leak sensitive information so
#' turning it off may increase user privacy. Read more about this header at
#' [MDN](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-DNS-Prefetch-Control)
#'
#' ### `X-Download-Options` (**UI**)
#' This is an old header only relevant to Internet Explorer 8 and below that
#' prevents downloaded content from having access to your site's context.
#'
#' ### `X-Frame-Options` (**UI**)
#' This header has been superseeded by the `frame-ancestor` directive in the
#' `Content-Security-Policy` header but may still be good to set for older
#' browsers. It controls whether a site is allowed to be rendered inside a frame
#' in another document. Preventing this can prevent click-jacking attacks. Read
#' more about this header at [MDN](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Frame-Options)
#'
#' ### `X-Permitted-Cross-Domain-Policies`
#' This header controls cross-origin access of a resource from a document
#' running in a web client such as Adobe Flash Player or Microsoft Silverlight.
#' The demise of these technologies have made this header less important. Read
#' more about this header at [MDN](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Permitted-Cross-Domain-Policies)
#'
#' ### `X-XSS-Protection` (**UI**)
#' This header has been deprecated in favor of the more powerful
#' `Content-Security-Policy` header. In fact using XSS filtering can incur a
#' security vulnerability which is why the default for SecurityHeaders is to turn the
#' feature off (by setting `X-XSS-Protection: 0` rather than omitting the
#' header). Read more about this header at
#' [MDN](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-XSS-Protection)
#'
#' @usage NULL
#' @format NULL
#'
#' @section Initialization:
#' A new 'SecurityHeaders'-object is initialized using the \code{new()} method on the
#' generator and pass in any settings deviating from the defaults
#'
#' \strong{Usage}
#' \tabular{l}{
#' \code{security_headers <- SecurityHeaders$new(...)}
#' }
#'
#' @section Fiery plugin:
#' A SecurityHeaders object is a fiery plugin and can be used by passing it to the
#' `attach()` method of the fiery server object. Once attached all requests
#' created will be prepopulated with the given headers. Any request handler is
#' permitted to remove one or more of the headers to opt out of them.
#'
#' @export
#'
#' @examples
#' # Create a plugin that turns off UI-related security headers
#' security_headers <- SecurityHeaders$new(
#' content_security_policy = NULL,
#' cross_origin_embedder_policy = NULL,
#' cross_origin_opener_policy = NULL,
#' origin_agent_cluster = NULL,
#' referrer_policy = NULL,
#' x_dns_prefetch_control = NULL,
#' x_download_options = NULL,
#' x_frame_options = NULL,
#' x_xss_protection = NULL
#' )
#'
#' @examplesIf requireNamespace("fiery", quietly = TRUE)
#' # Use it with a fiery server
#' app <- fiery::Fire$new()
#'
#' app$attach(security_headers)
#'
SecurityHeaders <- R6::R6Class(
"SecurityHeaders",
public = list(
#' @description Initialize a new SecurityHeaders object
#' @param content_security_policy Set the value of the `Content-Security-Policy`
#' header. See [csp()] for documentation of its values
#' @param content_security_policy_report_only Set the value of the
#' `Content-Security-Policy-Report-Only` header. See [csp()] for
#' documentation of its values
#' @param cross_origin_embedder_policy Set the value of the
#' `Cross-Origin-Embedder-Policy`. Possible values are `"unsafe-none"`,
#' `"require-corp"`, and `"credentialless"`
#' @param cross_origin_opener_policy Set the value of the
#' `Cross-Origin-Opener-Policy`. Possible values are `"unsafe-none"`,
#' `"same-origin-allow-popups"`, `"same-origin"`, and
#' `"noopener-allow-popups"`
#' @param cross_origin_resource_policy Set the value of the
#' `Cross-Origin-Resource-Policy`. Possible values are `"same-site"`,
#' `"same-origin"`, and `"cross-origin"`
#' @param origin_agent_cluster Set the value of the
#' `Origin-Agent-Cluster`. Possible values are `TRUE` and `FALSE`
#' @param referrer_policy Set the value of the
#' `Referrer-Policy`. Possible values are `"no-referrer"`,
#' `"no-referrer-when-downgrade"`, `"origin"`, `"origin-when-cross-origin"`,
#' `"same-origin"`, `"strict-origin"`, `"strict-origin-when-cross-origin"`,
#' and `"unsafe-url"`
#' @param strict_transport_security Set the value of the
#' `Strict-Transport-Security` header. See [sts()] for documentation of its
#' values
#' @param x_content_type_options Set the value of the
#' `X-Content-Type-Options`. Possible values are `TRUE` and `FALSE`
#' @param x_dns_prefetch_control Set the value of the
#' `X-DNS-Prefetch-Control`. Possible values are `TRUE` and `FALSE`
#' @param x_download_options Set the value of the
#' `X-Download-Options`. Possible values are `TRUE` and `FALSE`
#' @param x_frame_options Set the value of the
#' `X-Frame-Options`. Possible values are `"DENY"` and `"SAMEORIGIN"`
#' @param x_permitted_cross_domain_policies Set the value of the
#' `X-Permitted-Cross-Domain-Policies`. Possible values are `"none"`,
#' `"master-only"`, `"by-content-type"`, `"by-ftp-filename"`, `"all"`, and
#' `"none-this-response"`
#' @param x_xss_protection Set the value of the
#' `X-XSS-Protection`. Possible values are `TRUE` and `FALSE`
#'
initialize = function(
content_security_policy = csp(
default_src = "self",
script_src = "self",
script_src_attr = "none",
style_src = c("self", "https:", "unsafe-inline"),
img_src = c("self", "data:"),
font_src = c("self", "https:", "data:"),
object_src = "none",
base_uri = "self",
form_action = "self",
frame_ancestors = "self",
upgrade_insecure_requests = TRUE
),
content_security_policy_report_only = NULL,
cross_origin_embedder_policy = NULL,
cross_origin_opener_policy = "same-origin",
cross_origin_resource_policy = "same-origin",
origin_agent_cluster = TRUE,
referrer_policy = "no-referrer",
strict_transport_security = sts(
max_age = 63072000,
include_sub_domains = TRUE
),
x_content_type_options = TRUE,
x_dns_prefetch_control = FALSE,
x_download_options = TRUE,
x_frame_options = "SAMEORIGIN",
x_permitted_cross_domain_policies = "none",
x_xss_protection = FALSE
) {
self$content_security_policy <- content_security_policy
self$content_security_policy_report_only <- content_security_policy_report_only
self$cross_origin_embedder_policy <- cross_origin_embedder_policy
self$cross_origin_opener_policy <- cross_origin_opener_policy
self$cross_origin_resource_policy <- cross_origin_resource_policy
self$origin_agent_cluster <- origin_agent_cluster
self$referrer_policy <- referrer_policy
self$strict_transport_security <- strict_transport_security
self$x_content_type_options <- x_content_type_options
self$x_dns_prefetch_control <- x_dns_prefetch_control
self$x_download_options <- x_download_options
self$x_frame_options <- x_frame_options
self$x_permitted_cross_domain_policies <- x_permitted_cross_domain_policies
self$x_xss_protection <- x_xss_protection
},
#' @description Method for use by `fiery` when attached as a plugin. Should
#' not be called directly.
#' @param app The fiery server object
#' @param ... Ignored
#'
on_attach = function(app, ...) {
headers <- private$prepare_headers()
must_upgrade <- !is.null(self$strict_transport_security)
for (header in names(headers)) {
if (header == "reporting-endpoints") {
app$header(header, headers[[header]])
}
app$header(header, headers[[header]])
}
if (must_upgrade) {
if (is.null(app$plugins$header_routr)) {
rs <- routr::RouteStack$new()
rs$attach_to <- "header"
app$attach(rs)
}
upgrade_route <- routr::Route$new()
upgrade_route$add_handler(
"all",
"/*",
function(request, response, ...) {
if (request$protocol != "https") {
response <- request$respond()
response$status <- 308L
response$set_header(
"Location",
sub("^(\\w+?):", "^\\1s:", request$url)
)
response$remove_header("strict-transport-security")
FALSE
} else {
TRUE
}
}
)
app$plugins$header_routr$add_route(upgrade_route, "upgrade_protocol")
}
}
),
active = list(
#' @field content_security_policy Set or get the value of the
#' `Content-Security-Policy` header. See [csp()] for documentation of its
#' values
#'
content_security_policy = function(value) {
if (missing(value)) {
return(private$CSP)
}
value <- validate_csp(value)
private$CSP <- value
},
#' @field content_security_policy_report_only Set or get the value of the
#' `Content-Security-Policy-Report-Only` header. See [csp()] for
#' documentation of its values
#'
content_security_policy_report_only = function(value) {
if (missing(value)) {
return(private$CSPRO)
}
value <- validate_csp(value)
private$CSPRO <- value
},
#' @field cross_origin_embedder_policy Set or get the value of the
#' `Cross-Origin-Embedder-Policy`. Possible values are `"unsafe-none"`,
#' `"require-corp"`, and `"credentialless"`
#'
cross_origin_embedder_policy = function(value) {
if (missing(value)) {
return(private$COEP)
}
if (!is.null(value)) {
value <- arg_match0(
value,
c(
"unsafe-none",
"require-corp",
"credentialless"
)
)
}
private$COEP <- value
},
#' @field cross_origin_opener_policy Set or get the value of the
#' `Cross-Origin-Opener-Policy`. Possible values are `"unsafe-none"`,
#' `"same-origin-allow-popups"`, `"same-origin"`, and
#' `"noopener-allow-popups"`
#'
cross_origin_opener_policy = function(value) {
if (missing(value)) {
return(private$COOP)
}
if (!is.null(value)) {
value <- arg_match0(
value,
c(
"unsafe-none",
"same-origin-allow-popups",
"same-origin",
"noopener-allow-popups"
)
)
}
private$COOP <- value
},
#' @field cross_origin_resource_policy Set or get the value of the
#' `Cross-Origin-Resource-Policy`. Possible values are `"same-site"`,
#' `"same-origin"`, and `"cross-origin"`
#'
cross_origin_resource_policy = function(value) {
if (missing(value)) {
return(private$CORP)
}
if (!is.null(value)) {
value <- arg_match0(
value,
c("same-site", "same-origin", "cross-origin")
)
}
private$CORP <- value
},
#' @field origin_agent_cluster Set or get the value of the
#' `Origin-Agent-Cluster`. Possible values are `TRUE` and `FALSE`
#'
origin_agent_cluster = function(value) {
if (missing(value)) {
return(private$OAC)
}
check_bool(value, allow_null = TRUE)
private$OAC <- value
},
#' @field referrer_policy Set or get the value of the
#' `Referrer-Policy`. Possible values are `"no-referrer"`,
#' `"no-referrer-when-downgrade"`, `"origin"`, `"origin-when-cross-origin"`,
#' `"same-origin"`, `"strict-origin"`, `"strict-origin-when-cross-origin"`,
#' and `"unsafe-url"`
#'
referrer_policy = function(value) {
if (missing(value)) {
return(private$RP)
}
if (!is.null(value)) {
value <- arg_match0(
value,
c(
"no-referrer",
"no-referrer-when-downgrade",
"origin",
"origin-when-cross-origin",
"same-origin",
"strict-origin",
"strict-origin-when-cross-origin",
"unsafe-url"
)
)
}
private$RP <- value
},
#' @field strict_transport_security Set or get the value of the
#' `Strict-Transport-Security` header. See [sts()] for documentation of its
#' values
#'
strict_transport_security = function(value) {
if (missing(value)) {
return(private$STS)
}
if (!is.null(value)) {
if (
!(is_bare_list(value) &&
is_named(value) &&
all(
names(value) %in% c("max_age", "include_sub_domains", "preload")
))
) {
stop_input_type(
value,
"a list with elements `max_age`, `include_sub_domains`, and `preload`",
allow_null = TRUE
)
}
check_number_whole(value$max_age, min = 0, allow_infinite = FALSE)
check_bool(value$include_sub_domains, allow_null = TRUE)
check_bool(value$preload, allow_null = TRUE)
if (
isTRUE(value$preload) &&
!(value$max_age >= 31536000 && isTRUE(value$include_sub_domains))
) {
cli::cli_abort(
"{.arg preload} can only be set if {.code include_sub_domains == TRUE} and {.code max_age >= 31536000}"
)
}
}
private$STS <- value
},
#' @field x_content_type_options Set or get the value of the
#' `X-Content-Type-Options`. Possible values are `TRUE` and `FALSE`
#'
x_content_type_options = function(value) {
if (missing(value)) {
return(private$XCTO)
}
check_bool(value, allow_null = TRUE)
private$XCTO <- value
},
#' @field x_dns_prefetch_control Set or get the value of the
#' `X-DNS-Prefetch-Control`. Possible values are `TRUE` and `FALSE`
#'
x_dns_prefetch_control = function(value) {
if (missing(value)) {
return(private$XDPC)
}
check_bool(value, allow_null = TRUE)
private$XDPC <- value
},
#' @field x_download_options Set or get the value of the
#' `X-Download-Options`. Possible values are `TRUE` and `FALSE`
#'
x_download_options = function(value) {
if (missing(value)) {
return(private$XDO)
}
check_bool(value, allow_null = TRUE)
private$XDO <- value
},
#' @field x_frame_options Set or get the value of the
#' `X-Frame-Options`. Possible values are `"DENY"` and `"SAMEORIGIN"`
#'
x_frame_options = function(value) {
if (missing(value)) {
return(private$XFO)
}
if (!is.null(value)) {
value <- arg_match0(
toupper(value),
c("DENY", "SAMEORIGIN"),
"value"
)
}
private$XFO <- value
},
#' @field x_permitted_cross_domain_policies Set or get the value of the
#' `X-Permitted-Cross-Domain-Policies`. Possible values are `"none"`,
#' `"master-only"`, `"by-content-type"`, `"by-ftp-filename"`, `"all"`, and
#' `"none-this-response"`
#'
x_permitted_cross_domain_policies = function(value) {
if (missing(value)) {
return(private$XPCDP)
}
if (!is.null(value)) {
value <- arg_match0(
value,
c(
"none",
"master-only",
"by-content-type",
"by-ftp-filename",
"all",
"none-this-response"
)
)
}
private$XPCDP <- value
},
#' @field x_xss_protection Set or get the value of the
#' `X-XSS-Protection`. Possible values are `TRUE` and `FALSE`
x_xss_protection = function(value) {
if (missing(value)) {
return(private$XXP)
}
check_bool(value, allow_null = TRUE)
private$XXP <- value
},
#' @field name The name of the plugin
name = function() {
"security_headers"
}
),
private = list(
CSP = NULL,
CSPRO = NULL,
COEP = NULL,
COOP = NULL,
CORP = NULL,
OAC = NULL,
RP = NULL,
STS = NULL,
XCTO = NULL,
XDPC = NULL,
XDO = NULL,
XFO = NULL,
XPCDP = NULL,
XXP = NULL,
prepare_headers = function() {
headers <- list()
if (!is.null(self$content_security_policy)) {
csp <- self$content_security_policy
if ("report_to" %in% names(csp)) {
csp$report_uri <- csp$report_to
csp$report_to <- "csp-endpoint"
headers[["reporting-endpoints"]] <- paste0(
"csp-endpoint=\"",
csp$report_uri,
"\""
)
}
headers[["content-security-policy"]] <- paste(
gsub("_", "-", names(csp)),
vapply(csp, paste, character(1), collapse = " "),
collapse = "; "
)
}
if (!is.null(self$content_security_policy_report_only)) {
csp <- self$content_security_policy_report_only
if ("report_to" %in% names(csp)) {
csp$report_uri <- csp$report_to
csp$report_to <- "cspro-endpoint"
headers[["reporting-endpoints"]] <- paste0(
c(
headers[["reporting-endpoints"]],
paste0("cspro-endpoint=\"", csp$report_uri, "\"")
),
collapse = ", "
)
}
headers[["content-security-policy-report-only"]] <- paste(
gsub("_", "-", names(csp)),
vapply(csp, paste, character(1), collapse = " "),
collapse = "; "
)
}
headers[[
"cross-origin-embedder-policy"
]] <- self$cross_origin_embedder_policy
headers[["cross-origin-opener-policy"]] <- self$cross_origin_opener_policy
headers[[
"cross-origin-resource-policy"
]] <- self$cross_origin_resource_policy
if (!is.null(self$origin_agent_cluster)) {
headers[["origin-agent-cluster"]] <- if (self$origin_agent_cluster) {
"?1"
} else {
"?0"
}
}
headers[["referrer-policy"]] <- self$referrer_policy
if (!is.null(self$strict_transport_security)) {
headers[["strict-transport-security"]] <- paste0(
c(
paste0("max-age=", self$strict_transport_security$max_age),
if (isTRUE(self$strict_transport_security$include_sub_domains)) {
"includeSubDomain"
},
if (isTRUE(self$strict_transport_security$preload)) "preload"
),
collapse = "; "
)
}
if (isTRUE(self$x_content_type_options)) {
headers[["x-content-type-options"]] <- "nosniff"
}
if (!is.null(self$x_dns_prefetch_control)) {
headers[["x-dns-prefetch-control"]] <- if (
self$x_dns_prefetch_control
) {
"on"
} else {
"off"
}
}
if (isTRUE(self$x_download_options)) {
headers[["x-download-options"]] <- "noopen"
}
headers[["x-frame-options"]] <- self$x_frame_options
headers[[
"x-permitted-cross-domain-policies"
]] <- self$x_permitted_cross_domain_policies
if (!is.null(self$x_xss_protection)) {
headers[["x-xss-protection"]] <- if (self$x_xss_protection) {
"1; mode=block"
} else {
"0"
}
}
headers[lengths(headers) != 0]
}
)
)
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.