mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
278a8fccc9
commit
35407142d0
@ -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.
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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"`
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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, "")
|
||||
|
29
public/app/core/trustedTypePolicies.ts
Normal file
29
public/app/core/trustedTypePolicies.ts
Normal 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, '<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;
|
||||
},
|
||||
});
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import './core/trustedTypePolicies';
|
||||
declare let __webpack_public_path__: string;
|
||||
declare let __webpack_nonce__: string;
|
||||
|
||||
|
5
public/app/types/window.d.ts
vendored
5
public/app/types/window.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user