diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index b84e078534..3ec97403a8 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -119,16 +119,58 @@ else PENPOT_CSP_POLICY_IS_CUSTOM="false" fi -export PENPOT_CSP_POLICY=${PENPOT_CSP_POLICY:-"default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'wasm-unsafe-eval'${PENPOT_CSP_SCRIPT_HASHES}; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' blob: data:; worker-src 'self' blob:; media-src 'self' blob:; frame-src 'self'; manifest-src 'self'"} +# Directives a deployment may extend. The default policy carries hashes that +# change on every build, so a deployment that needs an extra origin cannot +# hardcode the whole policy without recomputing them at each release. These +# variables let it declare only what it adds. +# +# base-uri, form-action, object-src and frame-ancestors are deliberately not +# extensible: there is no legitimate reason to relax them, and doing so +# silently removes the protection they provide. A deployment that really +# needs it can still set PENPOT_CSP_POLICY and own the whole policy. +if [ "${PENPOT_CSP_POLICY_IS_CUSTOM}" = "false" ]; then + export PENPOT_CSP_POLICY="default-src 'self'\ +; base-uri 'self'\ +; object-src 'none'\ +; frame-ancestors 'self'\ +; form-action 'self'\ +; manifest-src 'self'\ +; script-src 'self' 'wasm-unsafe-eval'${PENPOT_CSP_SCRIPT_HASHES}${PENPOT_CSP_SCRIPT_SRC_EXTRA:+ ${PENPOT_CSP_SCRIPT_SRC_EXTRA}}\ +; style-src 'self' 'unsafe-inline'${PENPOT_CSP_STYLE_SRC_EXTRA:+ ${PENPOT_CSP_STYLE_SRC_EXTRA}}\ +; img-src 'self' data: blob:${PENPOT_CSP_IMG_SRC_EXTRA:+ ${PENPOT_CSP_IMG_SRC_EXTRA}}\ +; font-src 'self'${PENPOT_CSP_FONT_SRC_EXTRA:+ ${PENPOT_CSP_FONT_SRC_EXTRA}}\ +; connect-src 'self' blob: data:${PENPOT_CSP_CONNECT_SRC_EXTRA:+ ${PENPOT_CSP_CONNECT_SRC_EXTRA}}\ +; frame-src 'self'${PENPOT_CSP_FRAME_SRC_EXTRA:+ ${PENPOT_CSP_FRAME_SRC_EXTRA}}\ +; worker-src 'self' blob:\ +; media-src 'self' blob:${PENPOT_CSP_REPORT_URI:+; report-uri ${PENPOT_CSP_REPORT_URI}}" +else + for _var in SCRIPT_SRC_EXTRA STYLE_SRC_EXTRA IMG_SRC_EXTRA FONT_SRC_EXTRA \ + CONNECT_SRC_EXTRA FRAME_SRC_EXTRA REPORT_URI; do + eval "_value=\${PENPOT_CSP_${_var}:-}" + if [ -n "${_value}" ]; then + echo "penpot: WARNING: PENPOT_CSP_${_var} is ignored because PENPOT_CSP_POLICY defines the whole policy." >&2 + fi + done + unset _var _value + export PENPOT_CSP_POLICY +fi case "${PENPOT_CSP_MODE}" in enforce) export PENPOT_CSP_DIRECTIVE="add_header Content-Security-Policy \"${PENPOT_CSP_POLICY}\" always;" - if [ "${PENPOT_CSP_POLICY_IS_CUSTOM}" = "false" ]; then - echo "penpot: WARNING: PENPOT_CSP_MODE=enforce degrades the plugin runtime." >&2 - echo "penpot: it initialises on every page load and needs 'unsafe-eval', which the default policy does not grant." >&2 - echo "penpot: set PENPOT_CSP_POLICY to your own policy if you need plugins, or keep report-only." >&2 - fi + # The plugin runtime initialises on every page load, whether or not a + # plugin is opened, and its sandbox needs 'unsafe-eval'. Look at the + # policy that will actually be served rather than at where it came + # from, since it can be granted through the default policy, through + # PENPOT_CSP_SCRIPT_SRC_EXTRA or through a policy of your own. + case "${PENPOT_CSP_POLICY}" in + *"'unsafe-eval'"*) ;; + *) + echo "penpot: WARNING: PENPOT_CSP_MODE=enforce degrades the plugin runtime." >&2 + echo "penpot: it initialises on every page load and needs 'unsafe-eval', which this policy does not grant." >&2 + echo "penpot: add it through PENPOT_CSP_SCRIPT_SRC_EXTRA if you need plugins, or keep report-only." >&2 + ;; + esac ;; report-only) export PENPOT_CSP_DIRECTIVE="add_header Content-Security-Policy-Report-Only \"${PENPOT_CSP_POLICY}\" always;" diff --git a/docs/technical-guide/configuration.md b/docs/technical-guide/configuration.md index 07f77f186d..a8ddfc372b 100644 --- a/docs/technical-guide/configuration.md +++ b/docs/technical-guide/configuration.md @@ -468,17 +468,80 @@ OIDC single sign-on needs no exception: the provider is reached by navigating aw Penpot, which no directive of this policy governs, the response returns as a redirect, and both discovery and the token exchange happen on the backend rather than in the browser. -Set your own policy with `PENPOT_CSP_POLICY` if you need to relax or tighten it, for -example to allow plugins. Note that a custom policy replaces the default one entirely, -including the generated hashes, so take them from -`Content-Security-Policy-Report-Only` on a running container and paste them in place of -``. Note as well that `base-uri`, `form-action` and `frame-ancestors` have no -fallback to `default-src`, so a shorter policy silently loses them: +#### Extending the policy + +Most deployments need to add an origin rather than rewrite the policy: a plugin host, an +analytics endpoint, a corporate font server. Declare only the addition and the rest of the +default policy, hashes included, stays in place: ```bash -PENPOT_CSP_POLICY: "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval' ; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' https: blob: data:; worker-src 'self' blob:; media-src 'self' blob:; frame-src 'self' https:; manifest-src 'self'" +PENPOT_CSP_CONNECT_SRC_EXTRA: "https://analytics.example.com" ``` +The extensible directives are `script-src`, `style-src`, `img-src`, `font-src`, +`connect-src` and `frame-src`. `base-uri`, `form-action`, `object-src` and +`frame-ancestors` are not extensible, since relaxing them removes the protection they +provide and no ordinary deployment needs to. + +`PENPOT_CSP_REPORT_URI` adds a `report-uri` directive, which is how a deployment collects +violations from real traffic while the policy is still in report-only mode. + +#### Running plugins under an enforcing policy + +Plugins need four directives, and under enforcing mode a missing one fails quietly rather +than reporting an error. The symptoms are worth knowing: the sandbox refuses to start +without `script-src`, installing a plugin fails with a network error without +`connect-src`, its icon does not appear without `img-src`, and its interface stays blank +without `frame-src`. + +```bash +PENPOT_CSP_SCRIPT_SRC_EXTRA: "'unsafe-eval'" +PENPOT_CSP_CONNECT_SRC_EXTRA: "https://plugins.example.com" +PENPOT_CSP_IMG_SRC_EXTRA: "https://plugins.example.com" +PENPOT_CSP_FRAME_SRC_EXTRA: "https://plugins.example.com" +``` + +`'unsafe-eval'` is required because the plugin sandbox evaluates plugin code, and it +applies to the whole application rather than to plugins alone. Note also that the plugin +runtime initialises on every page load whether or not a plugin is opened, so without it +the sandbox reports violations even on a deployment where nobody uses plugins. + +Listing the origins explicitly restricts which plugins can run, which the browser then +enforces. A deployment that cannot know in advance where its users install plugins from +needs the permissive form instead: + +```bash +PENPOT_CSP_SCRIPT_SRC_EXTRA: "'unsafe-eval'" +PENPOT_CSP_CONNECT_SRC_EXTRA: "https:" +PENPOT_CSP_IMG_SRC_EXTRA: "https:" +PENPOT_CSP_FRAME_SRC_EXTRA: "https:" +``` + +#### Replacing the policy + +`PENPOT_CSP_POLICY` defines the whole policy and takes precedence, in which case the +variables above are ignored and a warning is logged at startup. + +Be aware that this also replaces the generated hashes, which change on every build. A +deployment that pins the whole policy has to recompute them at each release or the +application stops loading, so prefer the extension variables unless you really need to +remove a directive or add one the variables above do not cover. + +`upgrade-insecure-requests` is an example of the latter. To add it, read the policy the +container is currently serving and use it as the starting point: + +```bash +curl -sI https://penpot.example.com/ | grep -i content-security-policy +``` + +Then set the whole thing, with the hashes taken from that output: + +```bash +PENPOT_CSP_POLICY: "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; manifest-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-...' 'sha256-...'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' blob: data:; frame-src 'self'; worker-src 'self' blob:; media-src 'self' blob:; upgrade-insecure-requests" +``` + +Remember to repeat that step on every upgrade, since the hashes will have changed. +

Because of the above, enforce with the default policy suits deployments that do not use plugins. Anywhere else it needs a policy of your own.