diff options
author | Mike Crute <mike@crute.us> | 2023-10-18 12:45:07 -0700 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2023-10-18 12:45:07 -0700 |
commit | 925de3239c9eab61a3a1275a554508f46a172709 (patch) | |
tree | d4b3c7d0f4f88c3cbcfc5adb30c296d12a227826 | |
parent | 4ba637b3c3596d6fb7d073d70f23e4f421949bdd (diff) | |
download | golib-925de3239c9eab61a3a1275a554508f46a172709.tar.bz2 golib-925de3239c9eab61a3a1275a554508f46a172709.tar.xz golib-925de3239c9eab61a3a1275a554508f46a172709.zip |
echo: allow extending CSP at request-timeecho/v0.15.0
-rw-r--r-- | echo/middleware/csp.go | 109 |
1 files changed, 94 insertions, 15 deletions
diff --git a/echo/middleware/csp.go b/echo/middleware/csp.go index 14047eb..644dfe1 100644 --- a/echo/middleware/csp.go +++ b/echo/middleware/csp.go | |||
@@ -2,6 +2,8 @@ package middleware | |||
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "bytes" | 4 | "bytes" |
5 | "crypto/sha256" | ||
6 | "encoding/base64" | ||
5 | "encoding/json" | 7 | "encoding/json" |
6 | "fmt" | 8 | "fmt" |
7 | "net/url" | 9 | "net/url" |
@@ -13,7 +15,19 @@ import ( | |||
13 | "github.com/labstack/echo/v4/middleware" | 15 | "github.com/labstack/echo/v4/middleware" |
14 | ) | 16 | ) |
15 | 17 | ||
16 | const HeaderReportTo = "ReportTo" | 18 | const ( |
19 | HeaderReportTo = "ReportTo" | ||
20 | cspExtendContextKey = "__echomw_csp__csp_extend" | ||
21 | cspReplaceContextKey = "__echomw_csp__csp_replace" | ||
22 | ) | ||
23 | |||
24 | func ReplaceCSP(c echo.Context, csp *ContentSecurityPolicyConfig) { | ||
25 | c.Set(cspReplaceContextKey, csp) | ||
26 | } | ||
27 | |||
28 | func ExtendCSP(c echo.Context, csp *ContentSecurityPolicyConfig) { | ||
29 | c.Set(cspExtendContextKey, csp) | ||
30 | } | ||
17 | 31 | ||
18 | type ContentSecurityPolicyConfig struct { | 32 | type ContentSecurityPolicyConfig struct { |
19 | Skipper middleware.Skipper | 33 | Skipper middleware.Skipper |
@@ -46,6 +60,53 @@ type ContentSecurityPolicyConfig struct { | |||
46 | RequireTrustedTypesFor []CSPDirective `csp:"require-trusted-types-for"` // experimental | 60 | RequireTrustedTypesFor []CSPDirective `csp:"require-trusted-types-for"` // experimental |
47 | } | 61 | } |
48 | 62 | ||
63 | func mergeField[T any](a []T, b []T) []T { | ||
64 | if b != nil { | ||
65 | v := make([]T, len(a)+len(b)) | ||
66 | copy(v, a) | ||
67 | copy(v[len(a):], b) | ||
68 | return v | ||
69 | } else { | ||
70 | return a | ||
71 | } | ||
72 | } | ||
73 | |||
74 | // ExtendSimple returns a copy of the current policy extended with some | ||
75 | // other policy. Boolean fields are not merged and will retain the value | ||
76 | // of the base configuration. | ||
77 | func (c *ContentSecurityPolicyConfig) ExtendSimple(o *ContentSecurityPolicyConfig) *ContentSecurityPolicyConfig { | ||
78 | return &ContentSecurityPolicyConfig{ | ||
79 | Skipper: c.Skipper, | ||
80 | ReportOnly: c.ReportOnly, | ||
81 | UpgradeInsecureRequests: c.UpgradeInsecureRequests, | ||
82 | BlockAllMixedContent: c.BlockAllMixedContent, | ||
83 | DefaultSrc: mergeField(c.DefaultSrc, o.DefaultSrc), | ||
84 | ChildSrc: mergeField(c.ChildSrc, o.ChildSrc), | ||
85 | ConnectSrc: mergeField(c.ConnectSrc, o.ConnectSrc), | ||
86 | FontSrc: mergeField(c.FontSrc, o.FontSrc), | ||
87 | FrameSrc: mergeField(c.FrameSrc, o.FrameSrc), | ||
88 | ImageSrc: mergeField(c.ImageSrc, o.ImageSrc), | ||
89 | ManifestSrc: mergeField(c.ManifestSrc, o.ManifestSrc), | ||
90 | MediaSrc: mergeField(c.MediaSrc, o.MediaSrc), | ||
91 | ObjectSrc: mergeField(c.ObjectSrc, o.ObjectSrc), | ||
92 | ScriptSrc: mergeField(c.ScriptSrc, o.ScriptSrc), | ||
93 | StyleSrc: mergeField(c.StyleSrc, o.StyleSrc), | ||
94 | BaseUri: mergeField(c.BaseUri, o.BaseUri), | ||
95 | Sandbox: mergeField(c.Sandbox, o.Sandbox), | ||
96 | FormAction: mergeField(c.FormAction, o.FormAction), | ||
97 | FrameAncestors: mergeField(c.FrameAncestors, o.FrameAncestors), | ||
98 | ReportUri: mergeField(c.ReportUri, o.ReportUri), | ||
99 | ReportTo: mergeField(c.ReportTo, o.ReportTo), | ||
100 | PrefetchSrc: mergeField(c.PrefetchSrc, o.PrefetchSrc), | ||
101 | ScriptSrcElem: mergeField(c.ScriptSrcElem, o.ScriptSrcElem), | ||
102 | StyleSrcElem: mergeField(c.StyleSrcElem, o.StyleSrcElem), | ||
103 | StyleSrcAttr: mergeField(c.StyleSrcAttr, o.StyleSrcAttr), | ||
104 | WorkerSrc: mergeField(c.WorkerSrc, o.WorkerSrc), | ||
105 | NavigateTo: mergeField(c.NavigateTo, o.NavigateTo), | ||
106 | RequireTrustedTypesFor: mergeField(c.RequireTrustedTypesFor, o.RequireTrustedTypesFor), | ||
107 | } | ||
108 | } | ||
109 | |||
49 | func (c *ContentSecurityPolicyConfig) String() string { | 110 | func (c *ContentSecurityPolicyConfig) String() string { |
50 | st := reflect.TypeOf(*c) | 111 | st := reflect.TypeOf(*c) |
51 | sv := reflect.ValueOf(*c) | 112 | sv := reflect.ValueOf(*c) |
@@ -135,6 +196,11 @@ func CSPShaString(size int, h string) CSPDirective { | |||
135 | return CSPDirective(fmt.Sprintf("'sha%d-%s'", size, h)) | 196 | return CSPDirective(fmt.Sprintf("'sha%d-%s'", size, h)) |
136 | } | 197 | } |
137 | 198 | ||
199 | func CSPSha256FromBytes(d []byte) CSPDirective { | ||
200 | s := sha256.Sum256(d) | ||
201 | return CSPShaString(256, base64.StdEncoding.EncodeToString(s[:])) | ||
202 | } | ||
203 | |||
138 | type CSPSandbox string | 204 | type CSPSandbox string |
139 | 205 | ||
140 | const ( | 206 | const ( |
@@ -174,22 +240,35 @@ func ContentSecurityPolicyWithConfig(config ContentSecurityPolicyConfig) echo.Mi | |||
174 | return next(c) | 240 | return next(c) |
175 | } | 241 | } |
176 | 242 | ||
177 | h := c.Response().Header() | 243 | // This has to hook after the template runs but before the headers |
178 | if config.ReportOnly { | 244 | // are written because some template helper functions want to modify |
179 | h.Set(echo.HeaderContentSecurityPolicyReportOnly, config.String()) | 245 | // the CSP state and if it renders too early that won't work. |
180 | } else { | 246 | c.Response().Before(func() { |
181 | h.Set(echo.HeaderContentSecurityPolicy, config.String()) | 247 | liveConfig := &config |
182 | } | ||
183 | 248 | ||
184 | if config.ReportTo != nil { | 249 | if replace, ok := c.Get(cspReplaceContextKey).(*ContentSecurityPolicyConfig); ok { |
185 | rt := bytes.Buffer{} | 250 | liveConfig = replace |
186 | je := json.NewEncoder(&rt) | 251 | } else if extend, ok := c.Get(cspExtendContextKey).(*ContentSecurityPolicyConfig); ok { |
187 | for _, r := range config.ReportTo { | 252 | liveConfig = config.ExtendSimple(extend) |
188 | _ = je.Encode(r) | ||
189 | rt.WriteString(", ") | ||
190 | } | 253 | } |
191 | h.Set(HeaderReportTo, rt.String()) | 254 | |
192 | } | 255 | h := c.Response().Header() |
256 | if liveConfig.ReportOnly { | ||
257 | h.Set(echo.HeaderContentSecurityPolicyReportOnly, liveConfig.String()) | ||
258 | } else { | ||
259 | h.Set(echo.HeaderContentSecurityPolicy, liveConfig.String()) | ||
260 | } | ||
261 | |||
262 | if liveConfig.ReportTo != nil { | ||
263 | rt := bytes.Buffer{} | ||
264 | je := json.NewEncoder(&rt) | ||
265 | for _, r := range liveConfig.ReportTo { | ||
266 | _ = je.Encode(r) | ||
267 | rt.WriteString(", ") | ||
268 | } | ||
269 | h.Set(HeaderReportTo, rt.String()) | ||
270 | } | ||
271 | }) | ||
193 | 272 | ||
194 | return next(c) | 273 | return next(c) |
195 | } | 274 | } |