CVE-2024-30250 (Other, published 2024-04-04T15:15Z)
Description: value: Astro-Shield is an integration to enhance website security with SubResource Integrity hashes, Content-Security-Policy headers, and other techniques. Versions from 1.2.0 to 1.3.1 of Astro-Shield allow bypass to the allow-lists for cross-origin resources by introducing valid `integrity` attributes to the injected code. This implies that the injected SRI hash would be added to the generated CSP header, which would lead the browser to believe that the injected resource is legit. This vulnerability is patched in version 1.3.2.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/kindspells/astro-shield
Files changed:
.editorconfig
@kindspells/astro-shield/e2e/e2e.test.mts
@kindspells/astro-shield/e2e/fixtures/hybrid3/astro.config.mjs
@kindspells/astro-shield/e2e/fixtures/hybrid3/src/pages/injected.astro
@kindspells/astro-shield/src/core.mjs
@kindspells/astro-shield/tests/core.test.mts
Commit message: fix: ensure that allowed scripts are in hashes module

Signed-off-by: Andres Correa Casablanca <andreu@kindspells.dev>


Vulnerable code:
[*.md]
			enableStatic_SRI: true,
			enableMiddleware_SRI: true,
			sriHashesModule,
 * @param {Required<Pick<SRIOptions, 'scriptsAllowListUrls' | 'stylesAllowListUrls'>>} sri
	for (const scriptUrl of sri.scriptsAllowListUrls) {
	for (const styleUrl of sri.stylesAllowListUrls) {
		const hModule = /**
			@type {{
					[k in keyof HashesCollection]: HashesCollection[k] extends Set<string>
							? string[] | undefined
							: (k extends 'perPageSriHashes'
								? Record<string, { scripts: string[]; styles: string [] }>
								: Record<'scripts' | 'styles', Record<string, string>>)
		}} */ (await import(/* @vite-ignore */ sriHashesModule))
	await scanForNestedResources(logger, distDir, h)
	if (
		sri.enableStatic &&
		sri.hashesModule &&
		!(await doesFileExist(sri.hashesModule))
	) {
		const h = /** @satisfies {HashesCollection} */ {
			inlineScriptHashes: new Set(),
			inlineStyleHashes: new Set(),
			extScriptHashes: new Set(),
			extStyleHashes: new Set(),
			perPageSriHashes: new Map(),
			perResourceSriHashes: {
				scripts: new Map(),
				styles: new Map(),
			},
		// We generate a provisional hashes module. It won't contain the hashes for
		// resources created by Astro, but it can be useful nonetheless.
		await scanForNestedResources(logger, publicDir, h)
		await scanAllowLists(sri, h)
		await generateSRIHashesModule(
			logger,
			h,
			sri.hashesModule,
			false, // So we don't get redundant warnings
		)
				<script type="module" src="/core.mjs" integrity="sha256-iozyX5cgvSGJZLKhhN7CRl6tn/jC3vYkBm8jfGv4x78="></script>
				'sha256-iozyX5cgvSGJZLKhhN7CRl6tn/jC3vYkBm8jfGv4x78=',

Fixed code:
[*.{md,mdx}]
charset = utf-8
trim_trailing_whitespace = true
import type { HashesModule } from '#as/core.mjs'

	it('incorporates the allowed scripts into the generated hashes module', async () => {
		const hashesModulePath = resolve(hybridDir, 'src', 'generated', 'sri.mjs')
		assert(await doesFileExist(hashesModulePath))

		const hashesModule = (await import(hashesModulePath)) as HashesModule

		assert(
			Object.hasOwn(
				hashesModule.perResourceSriHashes.scripts,
				'https://code.jquery.com/jquery-3.7.1.slim.min.js',
			),
		)
		assert(
			Object.hasOwn(
				hashesModule.perResourceSriHashes.scripts,
				'https://code.jquery.com/ui/1.13.2/jquery-ui.min.js',
			),
		)
	})
			sri: {
				enableStatic: true,
				enableMiddleware: true,
				hashesModule: sriHashesModule,
				scriptsAllowListUrls: [
					'https://code.jquery.com/jquery-3.7.1.slim.min.js',
					'https://code.jquery.com/ui/1.13.2/jquery-ui.min.js',
				],
			},
---
/*
 * SPDX-FileCopyrightText: 2024 KindSpells Labs S.L.
 *
 * SPDX-License-Identifier: MIT
 */

export const prerender = false
import '../styles/main.css'
---
<!DOCTYPE html><html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<title>My Static Test Site</title>
		<style>h1 { color: red; }</style>
	</head>
	<body>
		<!-- The next script is whitelisted -->
		<script
			src="https://code.jquery.com/jquery-3.7.1.slim.min.js"
			integrity="sha256-kmHvs0B+OpCW5GVHUNjv9rOmY0IvSIRcf7zGUDTDQM8="
			crossorigin="anonymous"
		></script>
        <!-- The next script is whitelisted, but we let Astro-Shield obtain its integrity hash -->
		<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js" type="module"></script>

		<!-- The next script, although not malicious, is used to simulate an injection -->
		<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44="></script>
		<p>This document simulates a page showing an injected script</p>
	</body>
</html>
 * @typedef {{
 * 	[k in keyof HashesCollection]: HashesCollection[k] extends Set<string>
 * 	  ? string[] | undefined
 * 	  : (k extends 'perPageSriHashes'
 * 	    ? Record<string, { scripts: string[]; styles: string [] }>
 * 	    : Record<'scripts' | 'styles', Record<string, string>>)
 * }} HashesModule
 * @param {Pick<SRIOptions, 'scriptsAllowListUrls' | 'stylesAllowListUrls'>} sri
	for (const scriptUrl of sri.scriptsAllowListUrls ?? []) {
	for (const styleUrl of sri.stylesAllowListUrls ?? []) {
		const hModule = /** @type {HashesModule} */ (
			await import(/* @vite-ignore */ sriHashesModule)
		)
	await scanAllowLists(sri, h)
	await scanForNestedResources(logger, distDir, h)
	if (sri.enableStatic && sri.hashesModule) {
		let shouldRegenerateHashesModule = !(await doesFileExist(sri.hashesModule))

		if (!shouldRegenerateHashesModule) {
			try {
				const hashesModule = /** @type {HashesModule} */ (
					await import(sri.hashesModule)
				)

				for (const allowedScript of sri.scriptsAllowListUrls) {
					if (
						!Object.hasOwn(
							hashesModule.perResourceSriHashes.scripts,
							allowedScript,
						)
					) {
						shouldRegenerateHashesModule = true
						break
					}
				}
			} catch (err) {
				logger.warn(
					`Failed to load SRI hashes module "${sri.hashesModule}", it will be re-generated:\n\t${err}`,
				)
				shouldRegenerateHashesModule = true
			}
		if (shouldRegenerateHashesModule) {
			const h = /** @satisfies {HashesCollection} */ {
				inlineScriptHashes: new Set(),
				inlineStyleHashes: new Set(),
				extScriptHashes: new Set(),
				extStyleHashes: new Set(),
				perPageSriHashes: new Map(),
				perResourceSriHashes: {
					scripts: new Map(),
					styles: new Map(),
				},
			}

			// We generate a provisional hashes module. It won't contain the hashes for
			// resources created by Astro, but it can be useful nonetheless.
			await scanForNestedResources(logger, publicDir, h)
			await scanAllowLists(sri, h)
			await generateSRIHashesModule(
				logger,
				h,
				sri.hashesModule,
				false, // So we don't get redundant warnings
			)
		}
				<script type="module" src="/core.mjs" integrity="sha256-zOEqmAz4SCAi+TcSQgdhUuurJfrfnwWqtmdTOP+bBkc="></script>
				'sha256-zOEqmAz4SCAi+TcSQgdhUuurJfrfnwWqtmdTOP+bBkc=',