diff options
Diffstat (limited to 'echo/middleware/csp.go')
-rw-r--r-- | echo/middleware/csp.go | 196 |
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 @@ | |||
1 | package middleware | ||
2 | |||
3 | import ( | ||
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 | |||
16 | const HeaderReportTo = "ReportTo" | ||
17 | |||
18 | type 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 | |||
48 | func (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 | |||
88 | type CSPReportTo struct { | ||
89 | GroupName string | ||
90 | MaxAge time.Duration | ||
91 | Endpoints []*url.URL | ||
92 | } | ||
93 | |||
94 | func (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 | |||
107 | type CSPDirective string | ||
108 | |||
109 | const ( | ||
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 | |||
125 | func CSPHost(s string) CSPDirective { | ||
126 | return CSPDirective(s) | ||
127 | } | ||
128 | |||
129 | func CSPNonce(n string) CSPDirective { | ||
130 | return CSPDirective(fmt.Sprintf("'nonce-%s'", n)) | ||
131 | } | ||
132 | |||
133 | func CSPShaString(size int, h string) CSPDirective { | ||
134 | return CSPDirective(fmt.Sprintf("'sha%d-%s'", size, h)) | ||
135 | } | ||
136 | |||
137 | type CSPSandbox string | ||
138 | |||
139 | const ( | ||
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 | |||
156 | var DefaultContentSecurityPolicyConfig = ContentSecurityPolicyConfig{ | ||
157 | Skipper: middleware.DefaultSkipper, | ||
158 | DefaultSrc: []CSPDirective{CSPSelf, CSPData}, | ||
159 | } | ||
160 | |||
161 | func ContentSecurityPolicy() echo.MiddlewareFunc { | ||
162 | return ContentSecurityPolicyWithConfig(DefaultContentSecurityPolicyConfig) | ||
163 | } | ||
164 | |||
165 | func 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 | } | ||