mirror of
https://github.com/penpot/penpot.git
synced 2026-09-19 02:16:14 +00:00
🐳 Allow extending the CSP directives without replacing the policy
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
This commit is contained in:
parent
1e8acbb2db
commit
b402637fe4
@ -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;"
|
||||
|
||||
@ -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
|
||||
`<hashes>`. 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' <hashes>; 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.
|
||||
|
||||
<p class="advice">
|
||||
Because of the above, <code class="language-bash">enforce</code> with the default policy
|
||||
suits deployments that do not use plugins. Anywhere else it needs a policy of your own.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user