mirror of
https://github.com/grafana/grafana.git
synced 2026-02-03 20:49:50 -05:00
Feature: Introduce subresource integrity checks (SRI) for frontend assets (#100983)
* feat(featuremgmt): introduce feature toggle for enabling sri checks * feat(frontend): use assetSriChecks feature toggle to inject integrity hash into script tags * chore(webpack): align sri algorithms across dev and prod builds * docs(featuremgmt): update assetSriChecks to pass CI * docs(featuremgmt): fix more spelling complaints with assetSriChecks * Add crossorigin attribute * chore(webpack): add subresource-integrity plugin * build(webpack): wrap webpack jsonp loader integrity checks in feature flag checks * revert(index.html): remove crossorigin attribute if assertSriChecks is disabled --------- Co-authored-by: Kristian Bremberg <kristian.bremberg@grafana.com>
This commit is contained in:
parent
bf9a34f2ca
commit
bbfeb8d220
13 changed files with 141 additions and 7 deletions
|
|
@ -226,6 +226,7 @@ Experimental features might be changed or removed without prior notice.
|
|||
| `datasourceConnectionsTab` | Shows defined connections for a data source in the plugins detail page |
|
||||
| `newLogsPanel` | Enables the new logs panel in Explore |
|
||||
| `pluginsCDNSyncLoader` | Load plugins from CDN synchronously |
|
||||
| `assetSriChecks` | Enables SRI checks for Grafana JavaScript assets |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@
|
|||
"webpack-livereload-plugin": "3.0.2",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
"webpack-merge": "6.0.1",
|
||||
"webpack-subresource-integrity": "^5.2.0-rc.1",
|
||||
"webpackbar": "^7.0.0",
|
||||
"yaml": "^2.0.0",
|
||||
"yargs": "^17.5.1"
|
||||
|
|
|
|||
|
|
@ -254,4 +254,5 @@ export interface FeatureToggles {
|
|||
alertingRuleVersionHistoryRestore?: boolean;
|
||||
newShareReportDrawer?: boolean;
|
||||
rendererDisableAppPluginsPreload?: boolean;
|
||||
assetSriChecks?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const (
|
|||
grafanaBackendServicesSquad codeowner = "@grafana/grafana-backend-services-squad"
|
||||
grafanaSearchAndStorageSquad codeowner = "@grafana/search-and-storage"
|
||||
grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend"
|
||||
grafanaFrontendOpsWG codeowner = "@grafana/frontend-ops"
|
||||
grafanaAsCodeSquad codeowner = "@grafana/grafana-as-code"
|
||||
identityAccessTeam codeowner = "@grafana/identity-access-team"
|
||||
grafanaObservabilityLogsSquad codeowner = "@grafana/observability-logs"
|
||||
|
|
|
|||
|
|
@ -1775,6 +1775,13 @@ var (
|
|||
HideFromDocs: true,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "assetSriChecks",
|
||||
Description: "Enables SRI checks for Grafana JavaScript assets",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaFrontendOpsWG,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -235,3 +235,4 @@ alertingJiraIntegration,experimental,@grafana/alerting-squad,false,false,true
|
|||
alertingRuleVersionHistoryRestore,GA,@grafana/alerting-squad,false,false,true
|
||||
newShareReportDrawer,experimental,@grafana/sharing-squad,false,false,false
|
||||
rendererDisableAppPluginsPreload,experimental,@grafana/sharing-squad,false,false,true
|
||||
assetSriChecks,experimental,@grafana/frontend-ops,false,false,true
|
||||
|
|
|
|||
|
|
|
@ -950,4 +950,8 @@ const (
|
|||
// FlagRendererDisableAppPluginsPreload
|
||||
// Disable pre-loading app plugins when the request is coming from the renderer
|
||||
FlagRendererDisableAppPluginsPreload = "rendererDisableAppPluginsPreload"
|
||||
|
||||
// FlagAssetSriChecks
|
||||
// Enables SRI checks for Grafana JavaScript assets
|
||||
FlagAssetSriChecks = "assetSriChecks"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -598,6 +598,22 @@
|
|||
"codeowner": "@grafana/grafana-frontend-platform"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "assetSriChecks",
|
||||
"resourceVersion": "1739984409734",
|
||||
"creationTimestamp": "2025-02-19T15:56:59Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-02-19 17:00:09.734088 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables SRI checks for Grafana JavaScript assets",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/frontend-ops",
|
||||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "authAPIAccessTokenAuth",
|
||||
|
|
|
|||
|
|
@ -355,13 +355,26 @@
|
|||
<!-- End Google Tag Manager -->
|
||||
[[end]]
|
||||
|
||||
[[range $asset := .Assets.JSFiles]]
|
||||
<script
|
||||
nonce="[[$.Nonce]]"
|
||||
src="[[$asset.FilePath]]"
|
||||
type="text/javascript"
|
||||
defer
|
||||
></script>
|
||||
[[if .Settings.FeatureToggles.assetSriChecks ]]
|
||||
[[range $asset := .Assets.JSFiles]]
|
||||
<script
|
||||
nonce="[[$.Nonce]]"
|
||||
src="[[$asset.FilePath]]"
|
||||
integrity="[[$asset.Integrity]]"
|
||||
crossorigin="anonymous"
|
||||
type="text/javascript"
|
||||
defer
|
||||
></script>
|
||||
[[end]]
|
||||
[[else]]
|
||||
[[range $asset := .Assets.JSFiles]]
|
||||
<script
|
||||
nonce="[[$.Nonce]]"
|
||||
src="[[$asset.FilePath]]"
|
||||
type="text/javascript"
|
||||
defer
|
||||
></script>
|
||||
[[end]]
|
||||
[[end]]
|
||||
|
||||
<script nonce="[[.Nonce]]">
|
||||
|
|
|
|||
66
scripts/webpack/plugins/FeatureFlaggedSriPlugin.js
Normal file
66
scripts/webpack/plugins/FeatureFlaggedSriPlugin.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// @ts-check
|
||||
const webpack = require('webpack');
|
||||
|
||||
/** @typedef {import('webpack/lib/Compiler.js')} Compiler */
|
||||
|
||||
const PLUGIN_NAME = 'FeatureFlaggedSRIPlugin';
|
||||
const FEATURE_TOGGLE_WRAP = [
|
||||
'if (window.grafanaBootData && window.grafanaBootData.settings && window.grafanaBootData.settings.featureToggles && window.grafanaBootData.settings.featureToggles.assetSriChecks) {',
|
||||
'}',
|
||||
];
|
||||
|
||||
/**
|
||||
* Webpack plugin that wraps Webpack runtime integrity checks in a feature flag
|
||||
* This allows us to disable SRI checks in both the initial chunks but also in the
|
||||
* dynamically loaded chunks.
|
||||
*/
|
||||
class FeatureFlaggedSRIPlugin {
|
||||
/**
|
||||
* @param {Compiler} compiler The webpack compiler instance
|
||||
*/
|
||||
apply(compiler) {
|
||||
compiler.hooks.afterPlugins.tap(PLUGIN_NAME, (compiler) => {
|
||||
const logger = compiler.getInfrastructureLogger(PLUGIN_NAME);
|
||||
compiler.hooks.thisCompilation.tap(
|
||||
{
|
||||
name: PLUGIN_NAME,
|
||||
},
|
||||
(compilation) => {
|
||||
const { mainTemplate } = compilation;
|
||||
mainTemplate.hooks.jsonpScript.tap(
|
||||
PLUGIN_NAME,
|
||||
/**
|
||||
* @param {string} source
|
||||
*/
|
||||
(source) => {
|
||||
if (source.includes('script.integrity =')) {
|
||||
logger.log('FeatureFlaggedSRIPlugin: Wrapping SRI checks in feature flag');
|
||||
return createFeatureFlaggedSRITemplate(source);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a template string wrapping the integrity and crossorigin attributes in a feature flag
|
||||
* @param {string} source The original webpack template source
|
||||
* @returns {string} The modified template source
|
||||
*/
|
||||
function createFeatureFlaggedSRITemplate(source) {
|
||||
const lines = source.split('\n');
|
||||
const integrityAttributeLineNumber = lines.findIndex((line) => line.includes('script.integrity ='));
|
||||
const [prefix, suffix] = FEATURE_TOGGLE_WRAP;
|
||||
return webpack.Template.asString([
|
||||
...lines.slice(0, integrityAttributeLineNumber),
|
||||
prefix,
|
||||
webpack.Template.indent(lines.slice(integrityAttributeLineNumber)),
|
||||
suffix,
|
||||
]);
|
||||
}
|
||||
|
||||
module.exports = FeatureFlaggedSRIPlugin;
|
||||
|
|
@ -149,6 +149,7 @@ module.exports = (env = {}) => {
|
|||
new WebpackAssetsManifest({
|
||||
entrypoints: true,
|
||||
integrity: true,
|
||||
integrityHashes: ['sha384', 'sha512'],
|
||||
publicPath: true,
|
||||
}),
|
||||
new WebpackBar({
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ const { EnvironmentPlugin } = require('webpack');
|
|||
const WebpackAssetsManifest = require('webpack-assets-manifest');
|
||||
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
|
||||
const { merge } = require('webpack-merge');
|
||||
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');
|
||||
|
||||
const getEnvConfig = require('./env-util.js');
|
||||
const FeatureFlaggedSRIPlugin = require('./plugins/FeatureFlaggedSriPlugin');
|
||||
const common = require('./webpack.common.js');
|
||||
const esbuildTargets = resolveToEsbuildTarget(browserslist(), { printUnknownTargets: false });
|
||||
|
||||
|
|
@ -51,6 +53,9 @@ module.exports = (env = {}) =>
|
|||
}),
|
||||
],
|
||||
},
|
||||
output: {
|
||||
crossOriginLoading: 'anonymous',
|
||||
},
|
||||
optimization: {
|
||||
nodeEnv: 'production',
|
||||
minimize: parseInt(env.noMinify, 10) !== 1,
|
||||
|
|
@ -70,6 +75,8 @@ module.exports = (env = {}) =>
|
|||
new MiniCssExtractPlugin({
|
||||
filename: 'grafana.[name].[contenthash].css',
|
||||
}),
|
||||
new SubresourceIntegrityPlugin(),
|
||||
new FeatureFlaggedSRIPlugin(),
|
||||
/**
|
||||
* I know we have two manifest plugins here.
|
||||
* WebpackManifestPlugin was only used in prod before and does not support integrity hashes
|
||||
|
|
@ -77,6 +84,7 @@ module.exports = (env = {}) =>
|
|||
new WebpackAssetsManifest({
|
||||
entrypoints: true,
|
||||
integrity: true,
|
||||
integrityHashes: ['sha384', 'sha512'],
|
||||
publicPath: true,
|
||||
}),
|
||||
new WebpackManifestPlugin({
|
||||
|
|
|
|||
14
yarn.lock
14
yarn.lock
|
|
@ -18449,6 +18449,7 @@ __metadata:
|
|||
webpack-livereload-plugin: "npm:3.0.2"
|
||||
webpack-manifest-plugin: "npm:5.0.0"
|
||||
webpack-merge: "npm:6.0.1"
|
||||
webpack-subresource-integrity: "npm:^5.2.0-rc.1"
|
||||
webpackbar: "npm:^7.0.0"
|
||||
whatwg-fetch: "npm:3.6.20"
|
||||
yaml: "npm:^2.0.0"
|
||||
|
|
@ -31911,6 +31912,19 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webpack-subresource-integrity@npm:^5.2.0-rc.1":
|
||||
version: 5.2.0-rc.1
|
||||
resolution: "webpack-subresource-integrity@npm:5.2.0-rc.1"
|
||||
peerDependencies:
|
||||
html-webpack-plugin: ">= 5.0.0-beta.1 < 6"
|
||||
webpack: ^5.12.0
|
||||
peerDependenciesMeta:
|
||||
html-webpack-plugin:
|
||||
optional: true
|
||||
checksum: 10/a63e4e999812a753c70070457e6a2fb7e7ef1a05d8525a89796024fc56f26a0e56bd3e8f2b25e61f0ae969d627770a1fe326bebb58c296577d40c6a12aa4be37
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webpack-virtual-modules@npm:^0.5.0":
|
||||
version: 0.5.0
|
||||
resolution: "webpack-virtual-modules@npm:0.5.0"
|
||||
|
|
|
|||
Loading…
Reference in a new issue