Skip to content

Commit 21c7835

Browse files
committed
fix: implement aggressive inline style blocker for CSP compliance
- Add comprehensive React DOM operation patching - Block all inline style applications at multiple levels - Combine setProperty, setAttribute, createElement, and MutationObserver - Prevents CSP violations from React Bootstrap and BCGov components Fixes #1902
1 parent 86cef0f commit 21c7835

File tree

2 files changed

+169
-5
lines changed

2 files changed

+169
-5
lines changed

frontend/src/main.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ import { RouterProvider, createRouter } from '@tanstack/react-router'
66
// Import bootstrap styles
77
import '@/scss/styles.scss'
88

9-
// Import inline style interceptor - MUST be imported before React renders
10-
import { interceptInlineStyles } from '@/utils/intercept-inline-styles'
9+
// Import comprehensive style blocker - MUST be imported before React renders
10+
import { blockAllInlineStyles } from '@/utils/react-style-interceptor'
1111

1212
// Import the generated route tree
1313
import { routeTree } from './routeTree.gen'
1414

15-
// Intercept inline styles BEFORE React renders to prevent CSP violations
16-
// This catches inline styles from React Bootstrap, BCGov components, etc.
17-
interceptInlineStyles()
15+
// Block all inline styles BEFORE React renders to prevent CSP violations
16+
// This aggressively prevents inline styles from React Bootstrap, BCGov components, etc.
17+
// Our CSS classes handle all necessary styling
18+
blockAllInlineStyles()
1819

1920
// Create a new router instance
2021
const router = createRouter({ routeTree })
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/**
2+
* React Style Interceptor for CSP Compliance
3+
*
4+
* This module patches React's internal DOM manipulation to prevent inline styles
5+
* from being set. This must run before React renders any components.
6+
*/
7+
8+
/**
9+
* Patch React's DOM operations to filter out style attributes
10+
* This runs at a lower level than our previous interceptor
11+
*/
12+
export function patchReactDOMOperations(): () => void {
13+
if (typeof window === 'undefined' || typeof document === 'undefined') {
14+
return () => {}
15+
}
16+
17+
// Store original implementations
18+
const originalCreateElement = document.createElement.bind(document)
19+
20+
// Patch createElement to intercept style attributes at creation
21+
document.createElement = function (
22+
tagName: string,
23+
options?: ElementCreationOptions
24+
): HTMLElement {
25+
const element = originalCreateElement(tagName, options)
26+
27+
// Wrap the setAttribute method for this specific element
28+
const originalSetAttribute = element.setAttribute.bind(element)
29+
element.setAttribute = function (
30+
qualifiedName: string,
31+
value: string
32+
): void {
33+
if (qualifiedName.toLowerCase() === 'style') {
34+
// Silently ignore inline style attributes
35+
return
36+
}
37+
return originalSetAttribute(qualifiedName, value)
38+
}
39+
40+
// Wrap style property setter
41+
const styleDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'style')
42+
if (styleDescriptor && styleDescriptor.get) {
43+
const originalStyleGetter = styleDescriptor.get
44+
const styleObj = originalStyleGetter.call(element) as CSSStyleDeclaration
45+
46+
// Intercept setProperty on this element's style object
47+
const originalStyleSetProperty = styleObj.setProperty.bind(styleObj)
48+
styleObj.setProperty = function (
49+
property: string,
50+
value: string,
51+
priority?: string
52+
): void {
53+
// Silently ignore - CSS classes handle all styling
54+
return
55+
}
56+
}
57+
58+
return element
59+
}
60+
61+
// Return cleanup function
62+
return () => {
63+
document.createElement = originalCreateElement
64+
}
65+
}
66+
67+
/**
68+
* Comprehensive inline style blocker
69+
* Combines multiple interception methods
70+
*/
71+
export function blockAllInlineStyles(): () => void {
72+
if (typeof window === 'undefined') {
73+
return () => {}
74+
}
75+
76+
const cleanups: Array<() => void> = []
77+
78+
// Method 1: Patch CSSStyleDeclaration.setProperty globally
79+
const originalSetProperty = CSSStyleDeclaration.prototype.setProperty
80+
CSSStyleDeclaration.prototype.setProperty = function (
81+
property: string,
82+
value: string,
83+
priority?: string
84+
): void {
85+
// Block all inline style property setting
86+
return
87+
}
88+
cleanups.push(() => {
89+
CSSStyleDeclaration.prototype.setProperty = originalSetProperty
90+
})
91+
92+
// Method 2: Patch Element.setAttribute globally
93+
const originalSetAttribute = Element.prototype.setAttribute
94+
Element.prototype.setAttribute = function (
95+
qualifiedName: string,
96+
value: string
97+
): void {
98+
if (qualifiedName.toLowerCase() === 'style') {
99+
return
100+
}
101+
return originalSetAttribute.call(this, qualifiedName, value)
102+
}
103+
cleanups.push(() => {
104+
Element.prototype.setAttribute = originalSetAttribute
105+
})
106+
107+
// Method 3: Patch createElement
108+
const createElementCleanup = patchReactDOMOperations()
109+
cleanups.push(createElementCleanup)
110+
111+
// Method 4: MutationObserver to remove any style attributes that get set
112+
const observer = new MutationObserver((mutations) => {
113+
mutations.forEach((mutation) => {
114+
// Remove style attributes
115+
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
116+
const target = mutation.target as HTMLElement
117+
if (target.hasAttribute('style')) {
118+
target.removeAttribute('style')
119+
}
120+
}
121+
122+
// Check newly added nodes
123+
mutation.addedNodes.forEach((node) => {
124+
if (node.nodeType === Node.ELEMENT_NODE) {
125+
const element = node as HTMLElement
126+
if (element.hasAttribute('style')) {
127+
element.removeAttribute('style')
128+
}
129+
// Check all descendants
130+
const styledElements = element.querySelectorAll?.('[style]')
131+
styledElements?.forEach((el) => {
132+
el.removeAttribute('style')
133+
})
134+
}
135+
})
136+
})
137+
})
138+
139+
// Start observing when body is available
140+
const startObserving = () => {
141+
if (document.body) {
142+
observer.observe(document.body, {
143+
attributes: true,
144+
attributeFilter: ['style'],
145+
childList: true,
146+
subtree: true,
147+
})
148+
} else {
149+
setTimeout(startObserving, 10)
150+
}
151+
}
152+
startObserving()
153+
154+
cleanups.push(() => {
155+
observer.disconnect()
156+
})
157+
158+
// Return combined cleanup
159+
return () => {
160+
cleanups.forEach((cleanup) => cleanup())
161+
}
162+
}
163+

0 commit comments

Comments
 (0)