require-sri.ts 2.04 KB
Newer Older
1
import { Attribute, HtmlElement } from "../dom";
2
import { TagEndEvent } from "../event";
3
import { Rule, RuleDocumentation, ruleDocumentationUrl, SchemaObject } from "../rule";
4

5
6
7
8
9
10
11
type Target = "all" | "crossorigin";

interface RuleOptions {
	target: Target;
}

const defaults: RuleOptions = {
12
13
14
15
16
17
18
19
20
	target: "all",
};

const crossorigin = new RegExp("^(\\w+://|//)"); /* e.g. https:// or // */
const supportSri: { [key: string]: string } = {
	link: "href",
	script: "src",
};

21
export default class RequireSri extends Rule<void, RuleOptions> {
22
23
	private target: Target;

24
	public constructor(options: Partial<RuleOptions>) {
25
		super({ ...defaults, ...options });
26
27
28
		this.target = this.options.target;
	}

29
30
31
32
33
34
35
36
37
	public static schema(): SchemaObject {
		return {
			target: {
				enum: ["all", "crossorigin"],
				type: "string",
			},
		};
	}

38
39
40
41
42
43
44
	public documentation(): RuleDocumentation {
		return {
			description: `Subresource Integrity (SRI) \`integrity\` attribute is required to prevent manipulation from Content Delivery Networks or other third-party hosting.`,
			url: ruleDocumentationUrl(__filename),
		};
	}

45
	public setup(): void {
46
		this.on("tag:end", (event: TagEndEvent) => {
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
			/* only handle thats supporting and requires sri */
			const node = event.previous;
			if (!(this.supportSri(node) && this.needSri(node))) return;

			/* check if sri attribute is present */
			if (node.hasAttribute("integrity")) return;

			this.report(
				node,
				`SRI "integrity" attribute is required on <${node.tagName}> element`,
				node.location
			);
		});
	}

	private supportSri(node: HtmlElement): boolean {
		return Object.keys(supportSri).includes(node.tagName);
	}

	private needSri(node: HtmlElement): boolean {
		if (this.target === "all") return true;

		const attr = this.elementSourceAttr(node);
		if (!attr || attr.value === null || attr.isDynamic) {
			return false;
		}

		const url = attr.value.toString();
		return crossorigin.test(url);
	}

78
	private elementSourceAttr(node: HtmlElement): Attribute | null {
79
80
81
82
		const key = supportSri[node.tagName];
		return node.getAttribute(key);
	}
}