Feature: Trusted Types support (#64975)

* Draft: Feature: Trusted Types support

* remove trusted-types package

* Create policy before jQuery and Angular is loaded and add feature flag

* Add trustedTypePolicies

* Sanitize scriptURL

* Add TT meta tag for test env

* Move trusted types into core

* Add DOMParser support for TrustedHTML

* Seperate RSS sanitization and add better TrustedHTML support

* Get test CSP header from config

* Remove dompurify dep from core

* Add documentation for trusted types

* Apply suggestions from code review

Co-authored-by: Kristian Bremberg <114284895+KristianGrafana@users.noreply.github.com>

* Add comment about Github discussion thread and things breaking

* Remove changes from News panel

* Remove TT feature toggle

* Expose TT and CSPReportOnly to frontend

* Log errors in console when CSP report only is enabled

* Log error for reporting and remove test mode

* Only insert CSP header in HTML for dev env

* Update docs

---------

Co-authored-by: Tobias Skarhed <tobias.skarhed@gmail.com>
Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com>
This commit is contained in:
Kristian Bremberg 2023-04-27 18:20:37 +02:00 committed by GitHub
parent 278a8fccc9
commit 35407142d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 110 additions and 5 deletions

View File

@ -74,6 +74,24 @@ content_security_policy = true
content_security_policy_template = """script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
```
### Enable trusted types
**Currently in development. [Trusted types](https://github.com/w3c/trusted-types/blob/main/explainer.md) is an experimental Javascript API with [limited browser support](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types#browser_compatibility).**
Trusted types reduce the risk of DOM XSS by enforcing developers to sanitize strings that are used in injection sinks, such as setting `innerHTML` on an element. Furthermore, when enabling trusted types, these injection sinks need to go through a policy that will sanitize, or leave the string intact and return it as "safe". This provides some protection from client side injection vulnerabilities in third party libraries, such as jQuery, Angular and even third party plugins.
To enable trusted types in enforce mode, where injection sinks are automatically sanitized:
- Enable `content_security_policy` in the configuration.
- Add `require-trusted-types-for 'script'` to the `content_security_policy_template` in the configuration.
To enable trusted types in report mode, where inputs that have not been sanitized with trusted types will be logged to the console:
- Enable `content_security_policy_report_only` in the configuration.
- Add `require-trusted-types-for 'script'` to the `content_security_policy_report_only_template` in the configuration.
As this is a feature currently in development, things may break. If they do, or if you have any other feedback, feel free to [leave a comment](https://github.com/grafana/grafana/discussions/66823).
## Additional security hardening
The Grafana server has several built-in security features that you can opt-in to enhance security. This section describes additional techniques you can use to harden security.

View File

@ -120,7 +120,6 @@
"@types/d3-force": "^3.0.0",
"@types/d3-scale-chromatic": "3.0.0",
"@types/debounce-promise": "3.1.6",
"@types/dompurify": "^2",
"@types/eslint": "8.21.1",
"@types/file-saver": "2.0.5",
"@types/glob": "^8.0.0",
@ -293,6 +292,7 @@
"@sentry/utils": "6.19.7",
"@testing-library/react-hooks": "^8.0.1",
"@types/react-resizable": "3.0.3",
"@types/trusted-types": "2.0.3",
"@types/webpack-env": "1.18.0",
"@visx/event": "3.0.1",
"@visx/gradient": "3.0.0",

View File

@ -8,6 +8,8 @@ import {
sanitizeUrl,
sanitizeTextPanelContent,
sanitizeSVGContent,
sanitizeTrustedTypes,
sanitizeTrustedTypesRSS,
} from './sanitize';
export const textUtil = {
@ -17,4 +19,6 @@ export const textUtil = {
sanitizeTextPanelContent,
sanitizeUrl,
sanitizeSVGContent,
sanitizeTrustedTypes,
sanitizeTrustedTypesRSS,
};

View File

@ -47,6 +47,19 @@ export function sanitize(unsanitizedString: string): string {
}
}
export function sanitizeTrustedTypesRSS(unsanitizedString: string): TrustedHTML {
return DOMPurify.sanitize(unsanitizedString, {
RETURN_TRUSTED_TYPE: true,
ADD_ATTR: ['xmlns:atom', 'version', 'property', 'content'],
ADD_TAGS: ['rss', 'meta', 'channel', 'title', 'link', 'description', 'atom:link', 'item', 'pubDate', 'guid'],
PARSER_MEDIA_TYPE: 'application/xhtml+xml',
});
}
export function sanitizeTrustedTypes(unsanitizedString: string): TrustedHTML {
return DOMPurify.sanitize(unsanitizedString, { RETURN_TRUSTED_TYPE: true });
}
/**
* Returns string safe from XSS attacks to be used in the Text panel plugin.
*

View File

@ -198,6 +198,8 @@ export interface GrafanaConfig {
viewersCanEdit: boolean;
editorsCanAdmin: boolean;
disableSanitizeHtml: boolean;
trustedTypesDefaultPolicyEnabled: boolean;
cspReportOnlyEnabled: boolean;
liveEnabled: boolean;
/** @deprecated Use `theme2` instead. */
theme: GrafanaTheme;

View File

@ -81,6 +81,8 @@ export class GrafanaBootConfig implements GrafanaConfig {
viewersCanEdit = false;
editorsCanAdmin = false;
disableSanitizeHtml = false;
trustedTypesDefaultPolicyEnabled = false;
cspReportOnlyEnabled = false;
liveEnabled = true;
/** @deprecated Use `theme2` instead. */
theme: GrafanaTheme;

View File

@ -164,6 +164,8 @@ type FrontendSettingsDTO struct {
AngularSupportEnabled bool `json:"angularSupportEnabled"`
EditorsCanAdmin bool `json:"editorsCanAdmin"`
DisableSanitizeHtml bool `json:"disableSanitizeHtml"`
TrustedTypesDefaultPolicyEnabled bool `json:"trustedTypesDefaultPolicyEnabled"`
CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled"`
Auth FrontendSettingsAuthDTO `json:"auth"`

View File

@ -30,6 +30,9 @@ type IndexViewData struct {
Sentry *setting.Sentry
ContentDeliveryURL string
LoadingLogo template.URL
CSPContent string
CSPEnabled bool
IsDevelopmentEnv bool
// Nonce is a cryptographic identifier for use with Content Security Policy.
Nonce string
}

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"strings"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/plugins"
@ -89,6 +90,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
hasAccess := accesscontrol.HasAccess(hs.AccessControl, c)
secretsManagerPluginEnabled := kvstore.EvaluateRemoteSecretsPlugin(c.Req.Context(), hs.secretsPluginManager, hs.Cfg) == nil
trustedTypesDefaultPolicyEnabled := (hs.Cfg.CSPEnabled && strings.Contains(hs.Cfg.CSPTemplate, "require-trusted-types-for")) || (hs.Cfg.CSPReportOnlyEnabled && strings.Contains(hs.Cfg.CSPReportOnlyTemplate, "require-trusted-types-for"))
frontendSettings := &dtos.FrontendSettingsDTO{
DefaultDatasource: defaultDS,
@ -137,6 +139,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
AngularSupportEnabled: hs.Cfg.AngularSupportEnabled,
EditorsCanAdmin: hs.Cfg.EditorsCanAdmin,
DisableSanitizeHtml: hs.Cfg.DisableSanitizeHtml,
TrustedTypesDefaultPolicyEnabled: trustedTypesDefaultPolicyEnabled,
CSPReportOnlyEnabled: hs.Cfg.CSPReportOnlyEnabled,
DateFormats: hs.Cfg.DateFormats,
SecureSocksDSProxyEnabled: hs.Cfg.SecureSocksDSProxy.Enabled,

View File

@ -6,6 +6,7 @@ import (
"strings"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/middleware"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
@ -139,6 +140,12 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
Nonce: c.RequestNonce,
ContentDeliveryURL: hs.Cfg.GetContentDeliveryURL(hs.License.ContentDeliveryPrefix()),
LoadingLogo: "public/img/grafana_icon.svg",
IsDevelopmentEnv: hs.Cfg.Env == setting.Dev,
}
if hs.Cfg.CSPEnabled {
data.CSPEnabled = true
data.CSPContent = middleware.ReplacePolicyVariables(hs.Cfg.CSPTemplate, appURL, c.RequestNonce)
}
if !hs.AccessControl.IsDisabled() {

View File

@ -45,7 +45,7 @@ func nonceMiddleware(next http.Handler, logger log.Logger) http.Handler {
func cspMiddleware(cfg *setting.Cfg, next http.Handler, logger log.Logger) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := contexthandler.FromContext(req.Context())
policy := replacePolicyVariables(cfg.CSPTemplate, cfg.AppURL, ctx.RequestNonce)
policy := ReplacePolicyVariables(cfg.CSPTemplate, cfg.AppURL, ctx.RequestNonce)
rw.Header().Set("Content-Security-Policy", policy)
next.ServeHTTP(rw, req)
})
@ -54,13 +54,13 @@ func cspMiddleware(cfg *setting.Cfg, next http.Handler, logger log.Logger) http.
func cspReportOnlyMiddleware(cfg *setting.Cfg, next http.Handler, logger log.Logger) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := contexthandler.FromContext(req.Context())
policy := replacePolicyVariables(cfg.CSPReportOnlyTemplate, cfg.AppURL, ctx.RequestNonce)
policy := ReplacePolicyVariables(cfg.CSPReportOnlyTemplate, cfg.AppURL, ctx.RequestNonce)
rw.Header().Set("Content-Security-Policy-Report-Only", policy)
next.ServeHTTP(rw, req)
})
}
func replacePolicyVariables(policyTemplate, appURL, nonce string) string {
func ReplacePolicyVariables(policyTemplate, appURL, nonce string) string {
policy := strings.ReplaceAll(policyTemplate, "$NONCE", fmt.Sprintf("'nonce-%s'", nonce))
re := regexp.MustCompile(`^\w+:(//)?`)
rootPath := re.ReplaceAllString(appURL, "")

View File

@ -0,0 +1,29 @@
import { textUtil } from '@grafana/data';
import { config } from '@grafana/runtime';
const CSP_REPORT_ONLY_ENABLED = config.bootData.settings.cspReportOnlyEnabled;
if (
config.bootData.settings.trustedTypesDefaultPolicyEnabled &&
window.trustedTypes &&
window.trustedTypes.createPolicy
) {
// check if browser supports Trusted Types
window.trustedTypes.createPolicy('default', {
createHTML: (string, source, sink) => {
if (!CSP_REPORT_ONLY_ENABLED) {
return string.replace(/<script/gi, '&lt;script');
}
console.error('[HTML not sanitized with Trusted Types]', string, source, sink);
return string;
},
createScript: (string) => string,
createScriptURL: (string, source, sink) => {
if (!CSP_REPORT_ONLY_ENABLED) {
return textUtil.sanitizeUrl(string);
}
console.error('[ScriptURL not sanitized with Trusted Types]', string, source, sink);
return string;
},
});
}

View File

@ -1,3 +1,4 @@
import './core/trustedTypePolicies';
declare let __webpack_public_path__: string;
declare let __webpack_nonce__: string;

View File

@ -6,4 +6,9 @@ export declare global {
public_cdn_path: string;
nonce: string | undefined;
}
// Augment DOMParser to accept TrustedType sanitised content
interface DOMParser {
parseFromString(string: string | TrustedType, type: DOMParserSupportedType): Document;
}
}

View File

@ -1,6 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
[[ if and .CSPEnabled .IsDevelopmentEnv ]]
<!-- Cypress overwrites CSP headers in HTTP requests, so this is required for e2e tests-->
<meta http-equiv="Content-Security-Policy" content="[[.CSPContent]]"/>
[[ end ]]
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width" />

View File

@ -1,3 +1,7 @@
[security]
content_security_policy = true
content_security_policy_template = """require-trusted-types-for 'script'; script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
[feature_toggles]
enable = publicDashboards

View File

@ -10499,6 +10499,13 @@ __metadata:
languageName: node
linkType: hard
"@types/trusted-types@npm:2.0.3":
version: 2.0.3
resolution: "@types/trusted-types@npm:2.0.3"
checksum: 4794804bc4a4a173d589841b6d26cf455ff5dc4f3e704e847de7d65d215f2e7043d8757e4741ce3a823af3f08260a8d04a1a6e9c5ec9b20b7b04586956a6b005
languageName: node
linkType: hard
"@types/uglify-js@npm:*":
version: 3.13.1
resolution: "@types/uglify-js@npm:3.13.1"
@ -20222,7 +20229,6 @@ __metadata:
"@types/d3-force": ^3.0.0
"@types/d3-scale-chromatic": 3.0.0
"@types/debounce-promise": 3.1.6
"@types/dompurify": ^2
"@types/eslint": 8.21.1
"@types/file-saver": 2.0.5
"@types/glob": ^8.0.0
@ -20264,6 +20270,7 @@ __metadata:
"@types/slate-react": 0.22.9
"@types/testing-library__jest-dom": 5.14.5
"@types/tinycolor2": 1.4.3
"@types/trusted-types": 2.0.3
"@types/uuid": 9.0.1
"@types/webpack-env": 1.18.0
"@types/yargs": 17.0.22