aboutsummaryrefslogtreecommitdiff
path: root/echo/middleware/csp.go
diff options
context:
space:
mode:
Diffstat (limited to 'echo/middleware/csp.go')
-rw-r--r--echo/middleware/csp.go196
1 files changed, 196 insertions, 0 deletions
diff --git a/echo/middleware/csp.go b/echo/middleware/csp.go
new file mode 100644
index 0000000..60b4710
--- /dev/null
+++ b/echo/middleware/csp.go
@@ -0,0 +1,196 @@
1package middleware
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "net/url"
8 "reflect"
9 "strings"
10 "time"
11
12 "github.com/labstack/echo/v4"
13 "github.com/labstack/echo/v4/middleware"
14)
15
16const HeaderReportTo = "ReportTo"
17
18type ContentSecurityPolicyConfig struct {
19 Skipper middleware.Skipper
20 ReportOnly bool
21 DefaultSrc []CSPDirective `csp:"default-src"`
22 ChildSrc []CSPDirective `csp:"child-src"`
23 ConnectSrc []CSPDirective `csp:"connect-src"`
24 FontSrc []CSPDirective `csp:"font-src"`
25 FrameSrc []CSPDirective `csp:"frame-src"`
26 ImageSrc []CSPDirective `csp:"image-src"`
27 ManifestSrc []CSPDirective `csp:"manifest-src"`
28 MediaSrc []CSPDirective `csp:"media-src"`
29 ObjectSrc []CSPDirective `csp:"object-src"`
30 ScriptSrc []CSPDirective `csp:"script-src"`
31 StyleSrc []CSPDirective `csp:"style-src"`
32 BaseUri []CSPDirective `csp:"base-uri"`
33 Sandbox []CSPSandbox `csp:"sandbox"`
34 FormAction []CSPDirective `csp:"form-action"`
35 UpgradeInsecureRequests bool `csp:"upgrade-insecure-requests"`
36 FrameAncestors []CSPDirective `csp:"frame-ancestors"`
37 ReportUri []*url.URL `csp:"report-uri"` // deprecated
38 ReportTo []CSPReportTo `csp:"report-to"` // experimental
39 PrefetchSrc []CSPDirective `csp:"prefetch-src"` // experimental
40 ScriptSrcElem []CSPDirective `csp:"script-src-elem"` // experimental
41 StyleSrcElem []CSPDirective `csp:"style-src-elem"` // experimental
42 StyleSrcAttr []CSPDirective `csp:"script-src-attr"` // experimental
43 WorkerSrc []CSPDirective `csp:"worker-src"` // experimental
44 NavigateTo []CSPDirective `csp:"navigate-to"` // experimental
45 RequireTrustedTypesFor []CSPDirective `csp:"require-trusted-types-for"` // experimental
46}
47
48func (c *ContentSecurityPolicyConfig) String() string {
49 st := reflect.TypeOf(*c)
50 sv := reflect.ValueOf(*c)
51 lines := []string{}
52
53 for i := 0; i < st.NumField(); i++ {
54 cspTag := st.Field(i).Tag.Get("csp")
55 if cspTag == "" {
56 continue
57 }
58
59 v := sv.Field(i)
60 switch v.Kind() {
61 case reflect.Slice:
62 if v.Cap() == 0 {
63 continue
64 }
65 items := make([]string, v.Cap())
66 for j := 0; j < v.Cap(); j++ {
67 // Call the String() method if there is one to handle things
68 // like *net.URL instances. Otherwise just treat the value as a
69 // string because it probably is (all CSPDirective types are
70 // strings).
71 if str := v.Index(j).MethodByName("String"); str.IsValid() {
72 items[j] = str.Call(nil)[0].String()
73 } else {
74 items[j] = v.Index(j).String()
75 }
76 }
77 lines = append(lines, fmt.Sprintf("%s %s", cspTag, strings.Join(items, " ")))
78 case reflect.Bool:
79 if v.Bool() {
80 lines = append(lines, cspTag)
81 }
82 }
83 }
84
85 return strings.Join(lines, "; ") + ";"
86}
87
88type CSPReportTo struct {
89 GroupName string
90 MaxAge time.Duration
91 Endpoints []*url.URL
92}
93
94func (r CSPReportTo) MarshalJSON() ([]byte, error) {
95 ep := []map[string]string{}
96 for _, u := range r.Endpoints {
97 ep = append(ep, map[string]string{"url": u.String()})
98 }
99
100 return json.Marshal(map[string]interface{}{
101 "group": r.GroupName,
102 "max_age": r.MaxAge.Seconds(),
103 "endpoints": ep,
104 })
105}
106
107type CSPDirective string
108
109const (
110 CSPNone CSPDirective = "'none'"
111 CSPSelf = "'self'"
112 CSPUnsafeInline = "'unsafe-inline'"
113 CSPUnsafeEval = "'unsafe-eval'"
114 CSPUnsafeHashes = "'unsafe-hashes'"
115 CSPStrictDynamic = "'strict-dynamic'"
116 CSPReportSample = "'report-sample'"
117 CSPData = "data:"
118 CSPBlob = "blob:"
119 CSPMediastream = "mediastream:"
120 CSPFilesystem = "filesystem:"
121 CSPHttp = "http:"
122 CSPHttps = "https:"
123)
124
125func CSPHost(s string) CSPDirective {
126 return CSPDirective(s)
127}
128
129func CSPNonce(n string) CSPDirective {
130 return CSPDirective(fmt.Sprintf("'nonce-%s'", n))
131}
132
133func CSPShaString(size int, h string) CSPDirective {
134 return CSPDirective(fmt.Sprintf("'sha%d-%s'", size, h))
135}
136
137type CSPSandbox string
138
139const (
140 CSPAllowDownloads CSPSandbox = "allow-downloads"
141 CSPAllowDownloadsNoUser = "allow-downloads-without-user-activation"
142 CSPAllowForms = "allow-forms"
143 CSPAllowModals = "allow-modals"
144 CSPAllowOrientationLock = "allow-orientation-lock"
145 CSPAllowPointerLock = "allow-pointer-lock"
146 CSPAllowPopups = "allow-popups"
147 CSPAllowPopupEscape = "allow-popups-to-escape-sandbox"
148 CSPAllowPresentation = "allow-presentation"
149 CSPAllowSameOrigin = "allow-same-origin"
150 CSPAllowScripts = "allow-scripts"
151 CSPAllowStorageAccessByUser = "allow-storage-access-by-user-activation"
152 CSPAllowTopActivation = "allow-top-navigation"
153 CSPAllowNavigationByUser = "allow-top-navigation-by-user-activation"
154)
155
156var DefaultContentSecurityPolicyConfig = ContentSecurityPolicyConfig{
157 Skipper: middleware.DefaultSkipper,
158 DefaultSrc: []CSPDirective{CSPSelf, CSPData},
159}
160
161func ContentSecurityPolicy() echo.MiddlewareFunc {
162 return ContentSecurityPolicyWithConfig(DefaultContentSecurityPolicyConfig)
163}
164
165func ContentSecurityPolicyWithConfig(config ContentSecurityPolicyConfig) echo.MiddlewareFunc {
166 if config.Skipper == nil {
167 config.Skipper = DefaultContentSecurityPolicyConfig.Skipper
168 }
169
170 return func(next echo.HandlerFunc) echo.HandlerFunc {
171 return func(c echo.Context) error {
172 if config.Skipper(c) {
173 return next(c)
174 }
175
176 h := c.Response().Header()
177 if config.ReportOnly {
178 h.Set(echo.HeaderContentSecurityPolicyReportOnly, config.String())
179 } else {
180 h.Set(echo.HeaderContentSecurityPolicy, config.String())
181 }
182
183 if config.ReportTo != nil {
184 rt := bytes.Buffer{}
185 je := json.NewEncoder(&rt)
186 for _, r := range config.ReportTo {
187 _ = je.Encode(r)
188 rt.WriteString(", ")
189 }
190 h.Set(HeaderReportTo, rt.String())
191 }
192
193 return next(c)
194 }
195 }
196}