Skip to content

Commit

Permalink
Feat/override (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
RockiRider authored Sep 24, 2024
1 parent 9e8bbcf commit 5e50e8f
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 71 deletions.
7 changes: 6 additions & 1 deletion apps/docs/pages/api-docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,9 @@ You can read our guide for creating a policy [here](/guides/policy)
The reason this is set to false by default is because you might not need to hash your applications JS code for production. Inline scripts inside your `index.html` are hashed regardless of this setting.
</Callout>
</Tabs.Tab>
</Tabs>
</Tabs>

### override
- Type: `boolean`
- Default: `false`
- Description: This is a flag to override the default policy. When set to false, the plugin will merge the default policy (provided by the plugin) with your policy. When set to true, the plugin will **only** use your policy only.
17 changes: 17 additions & 0 deletions apps/react/tests/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ test("JQuery is blocked by CSP", async ({ page }) => {
expect(cspViolationDetected).toBe(true);
});

test("Override flag is working in plugin", async ({ page }) => {
await page.goto("/");
await expect(page.locator(`text=${TITLE}`)).toBeVisible();

//Get the meta tag and read the "font-src" attribute, which should contain the value "https://fonts.gstatic.com"
//And it shouldn't contain "img-src" attribute at all.

const metaElement = page.locator(
'meta[http-equiv="Content-Security-Policy"]'
);

const content = await metaElement.getAttribute("content");

expect(content).toContain("font-src https://fonts.gstatic.com");
expect(content).not.toContain("img-src");
});

test("Inline script is blocked by CSP", async ({ page }) => {
let cspViolationDetected = false;
let inlineScriptExecuted = false;
Expand Down
5 changes: 5 additions & 0 deletions apps/react/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export default defineConfig({
dev: {
run: true,
},
policy: {
"font-src": ["https://fonts.gstatic.com"],
"script-src-elem": ["'self'"],
},
override: true,
}),
],
preview: {
Expand Down
50 changes: 32 additions & 18 deletions packages/vite-plugin-csp-guard/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Plugin, ViteDevServer } from "vite";
import { PluginContext } from "rollup";
import { MyPluginOptions, TransformationStatus } from "./types";
import { DEFAULT_POLICY } from "./policy/constants";
import { calculateSkip, createNewCollection } from "./policy/core";
import { DEFAULT_DEV_POLICY, DEFAULT_POLICY } from "./policy/constants";
import {
calculateSkip,
createNewCollection,
mergePolicies,
overrideChecker,
} from "./policy/core";
import { transformHandler, transformIndexHtmlHandler } from "./transform";
import {
cssFilter,
jsFilter,
mergePolicies,
parseOutliers,
preCssFilter,
tsFilter,
Expand All @@ -20,25 +24,32 @@ export default function vitePluginCSP(
): Plugin {
const {
algorithm = "sha256",
policy = DEFAULT_POLICY,
policy,
dev = {},
features = FEATURE_FLAGS,
build = {},
override = false,
} = options;
let pluginContext: PluginContext | undefined = undefined; //Needed for logging
let isDevMode = false; // This is a flag to check if we are in dev mode
let server: ViteDevServer | undefined = undefined;

const { outlierSupport = [], run = false } = dev;
const { hash = false } = build;

const CORE_COLLECTION = createNewCollection();

const effectivePolicy = mergePolicies(DEFAULT_POLICY, policy);

let isDevMode = false; // This is a flag to check if we are in dev mode
const overrideIsFine = overrideChecker({
userPolicy: policy,
override,
});
if (!overrideIsFine) {
throw new Error(
"Override cannot be true when a csp policy is not provided"
);
}
const isUserDevOpt = run; // This is a flag to check if the user wants to run in dev mode
const canRunInDevMode = () => isDevMode && isUserDevOpt; // This is a function to check if we can run in dev mode
let pluginContext: PluginContext | undefined = undefined; //Needed for logging

let server: ViteDevServer | undefined = undefined;
const isDevAndAllowed = () => isDevMode && isUserDevOpt; // This is a function to check if we can run in dev mode

const transformationStatus: TransformationStatus = new Map<string, boolean>();
const isTransformationStatusEmpty = () => transformationStatus.size === 0;
Expand Down Expand Up @@ -86,7 +97,7 @@ export default function vitePluginCSP(
}
},
load(id) {
if (!canRunInDevMode()) return null; // Exit early if we are not in dev mode or if we are in dev mode but the user does not want to run in dev mode
if (!isDevAndAllowed()) return null; // Exit early if we are not in dev mode or if we are in dev mode but the user does not want to run in dev mode

// Entry points to files that need to be transformed
const isCss = cssFilter(id);
Expand All @@ -105,7 +116,7 @@ export default function vitePluginCSP(
console.log(id);
}

if (!canRunInDevMode()) return null; // Exit early if we are not in dev mode or if we are in dev mode but the user does not want to run in dev mode
if (!isDevAndAllowed()) return null; // Exit early if we are not in dev mode or if we are in dev mode but the user does not want to run in dev mode

await transformHandler({
code,
Expand All @@ -125,25 +136,28 @@ export default function vitePluginCSP(
if (features.mpa) {
console.log("transformIndexHtml");
}

const defaultPolicy = isDevAndAllowed()
? DEFAULT_DEV_POLICY
: DEFAULT_POLICY;

const effectivePolicy = mergePolicies(defaultPolicy, policy, override);

return transformIndexHtmlHandler({
html,
context,
algorithm,
policy: effectivePolicy,
collection: CORE_COLLECTION,
pluginContext,
canRunInDevMode: canRunInDevMode(),
isTransformationStatusEmpty: isTransformationStatusEmpty(),
isHashing: hash,
shouldSkip,
});
},
},
onLog(_level, log) {
if (
log.plugin === "vite-plugin-csp-guard" &&
log.pluginCode === "WARNING_CODE"
) {
if (log.plugin === "vite-plugin-csp-guard") {
this.warn(log);
}
},
Expand Down
48 changes: 47 additions & 1 deletion packages/vite-plugin-csp-guard/src/policy/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
HashCollection,
HashCollectionKey,
HashDataCollection,
OverrideCheckerProps,
ShouldSkip,
WarnMissingPolicyProps,
} from "../types";
Expand Down Expand Up @@ -59,6 +60,50 @@ export const addHash = ({ hash, key, data, collection }: AddHashProps) => {
}
};

export const overrideChecker = ({
userPolicy,
override,
}: OverrideCheckerProps) => {
const userPolicyExists = userPolicy && Object.keys(userPolicy).length > 0;
if (override && !userPolicyExists) {
return false;
}
return true;
};

export const mergePolicies = (
basePolicy: CSPPolicy,
newPolicy: CSPPolicy | undefined,
shouldOverride: boolean
): CSPPolicy => {
const newPolicyExists = newPolicy && Object.keys(newPolicy).length > 0;

if (shouldOverride) {
return newPolicy as CSPPolicy;
}
if (!newPolicyExists) return basePolicy;

const mergedPolicy: CSPPolicy = { ...basePolicy };

for (const key in newPolicy as CSPPolicy) {
const _key = key as keyof CSPPolicy;
if (newPolicy.hasOwnProperty(key)) {
const defaultValues = basePolicy[_key] || [];
const userValues = newPolicy[_key] || [];

if (Array.isArray(userValues)) {
mergedPolicy[_key] = Array.from(
new Set([...defaultValues, ...userValues])
);
} else {
mergedPolicy[_key] = userValues;
}
}
}

return mergedPolicy;
};

export const warnMissingPolicy = ({
currentPolicy,
source,
Expand All @@ -78,7 +123,7 @@ export const warnMissingPolicy = ({
}
};

export const calculateSkip = (policy: CSPPolicy): ShouldSkip => {
export const calculateSkip = (policy: CSPPolicy | undefined): ShouldSkip => {
const defaultShouldSkip = {
"script-src": false,
"script-src-attr": false,
Expand All @@ -87,6 +132,7 @@ export const calculateSkip = (policy: CSPPolicy): ShouldSkip => {
"style-src-attr": false,
"style-src-elem": false,
};
if (!policy) return defaultShouldSkip;

const keysToCheck = Object.keys(defaultShouldSkip) as (keyof ShouldSkip)[];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,5 @@ export function handleIndexHtml({
// });
// });
// }
console.log($.html());
return { HASH_COLLECTION, html: $.html() };
}
22 changes: 3 additions & 19 deletions packages/vite-plugin-csp-guard/src/transform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
} from "../types";
import { handleIndexHtml } from "./handleIndexHtml";
import { PluginContext } from "rollup";
import { DEFAULT_DEV_POLICY } from "../policy/constants";
import { generatePolicyString, policyToTag } from "../policy/createPolicy";
import { cssFilter, jsFilter, preCssFilter, tsFilter } from "../utils";
import { getCSS } from "../css/extraction";
Expand Down Expand Up @@ -111,9 +110,8 @@ export interface TransformIndexHtmlHandlerProps {
collection: HashCollection;
policy: CSPPolicy;
pluginContext: PluginContext | undefined;
canRunInDevMode: Boolean;
isTransformationStatusEmpty: Boolean;
isHashing: Boolean;
isTransformationStatusEmpty: boolean;
isHashing: boolean;
shouldSkip: ShouldSkip;
}

Expand All @@ -124,7 +122,6 @@ export const transformIndexHtmlHandler = async ({
policy,
collection,
pluginContext,
canRunInDevMode,
isTransformationStatusEmpty,
isHashing,
shouldSkip,
Expand Down Expand Up @@ -204,22 +201,9 @@ export const transformIndexHtmlHandler = async ({
}
);

const finalPolicy = { ...policy };

if (canRunInDevMode) {
const defaultDevPolicy = DEFAULT_DEV_POLICY;

for (const [key, defaultValues] of Object.entries(defaultDevPolicy)) {
const currentPolicy = finalPolicy[key as keyof CSPPolicy] ?? [];
finalPolicy[key as keyof CSPPolicy] = Array.from(
new Set([...currentPolicy, ...defaultValues])
);
}
}

const policyString = generatePolicyString({
collection: updatedCollection,
policy: finalPolicy,
policy: policy,
});

const InjectedHtmlTags = policyToTag(policyString);
Expand Down
10 changes: 10 additions & 0 deletions packages/vite-plugin-csp-guard/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ export type MyPluginOptions = {
*/
cssInJs?: boolean;
};
/**
* This is a flag to override the default policy. When set to false, the plugin will merge the default policy (provided by the plugin) with your policy. When set to true, the plugin will **only** use your policy only.
* @default false
*/
override?: boolean;
/**
* Options that apply only when running `vite dev`.
*/
Expand Down Expand Up @@ -141,6 +146,11 @@ export type WarnMissingPolicyProps = {
context?: PluginContext;
};

export type OverrideCheckerProps = {
userPolicy: CSPPolicy | undefined;
override: boolean;
};

export type TransformationStatus = Map<string, boolean>;

export type ShouldSkip = {
Expand Down
29 changes: 1 addition & 28 deletions packages/vite-plugin-csp-guard/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createFilter } from "vite";
import { CSPPolicy, Outlier, WarnMissingPolicyProps } from "./types";
import { Outlier, WarnMissingPolicyProps } from "./types";
import { REQUIRE_POST_TRANSFORM } from "./transform/constants";

export const extractBaseURL = (url: string): string | false => {
Expand Down Expand Up @@ -36,33 +36,6 @@ export const jsFilter = createFilter(["**/*.js?(*)", "**/*.jsx?(*)"]);
export const tsFilter = createFilter(["**/*.ts", "**/*.tsx"]);
export const htmlFilter = createFilter("**.html");

export const mergePolicies = (
defaultPolicy: CSPPolicy,
userPolicy: CSPPolicy | undefined
): CSPPolicy => {
if (!userPolicy) return defaultPolicy;

const mergedPolicy: CSPPolicy = { ...defaultPolicy };

for (const key in userPolicy as CSPPolicy) {
const _key = key as keyof CSPPolicy;
if (userPolicy.hasOwnProperty(key)) {
const defaultValues = defaultPolicy[_key] || [];
const userValues = userPolicy[_key] || [];

if (Array.isArray(userValues)) {
mergedPolicy[_key] = Array.from(
new Set([...defaultValues, ...userValues])
);
} else {
mergedPolicy[_key] = userValues;
}
}
}

return mergedPolicy;
};

export const parseOutliers = (outliers: Array<Outlier>) => {
return {
postTransform: outliers.some((outlier) =>
Expand Down
Loading

0 comments on commit 5e50e8f

Please sign in to comment.