Skip to content

Commit c37edb5

Browse files
authored
fix: injectScript now always injects a new script element, copying attributes from existing tags for CSP compliance and to prevent async loading race conditions. (#99)
1 parent 6001ae0 commit c37edb5

File tree

2 files changed

+48
-9
lines changed

2 files changed

+48
-9
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@googleworkspace/drive-picker-element": patch
3+
---
4+
5+
Fix: `injectScript` now always injects a new script element to avoid race conditions with `async` loading. It also copies all attributes (including `nonce` and `integrity`) from any existing script tag to ensure CSP compliance.

packages/drive-picker-element/src/utils.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,53 @@ export async function requestAccessToken(
5757
});
5858
}
5959

60+
/**
61+
* Injects a script into the document head.
62+
*
63+
* This function always creates and appends a new script element, even if a script with the same src
64+
* already exists. This is done to avoid race conditions where an existing script might have already
65+
* loaded (or failed) before we could attach event listeners, causing the promise to hang indefinitely.
66+
*
67+
* To ensure compatibility with Content Security Policy (CSP) and other requirements, this function
68+
* copies all attributes (including `nonce`, `integrity`, `crossorigin`, etc.) from any existing
69+
* script tag with the same src to the new script tag.
70+
*
71+
* Note: The script at the provided `src` must be idempotent, as it may be executed multiple times
72+
* if it is already present in the document.
73+
*/
6074
export async function injectScript(src: string): Promise<void> {
6175
return new Promise((resolve, reject) => {
62-
if (!document.querySelector(`script[src="${src}"]`)) {
63-
document.head.appendChild(
64-
Object.assign(document.createElement("script"), {
65-
src,
66-
onload: resolve,
67-
onerror: reject,
68-
}),
76+
const newScript = document.createElement("script");
77+
newScript.src = src;
78+
newScript.onload = () => resolve();
79+
newScript.onerror = (err) => reject(err);
80+
81+
// Check for an existing script to copy attributes from.
82+
// This is important for CSP compliance (nonce, integrity) and other attributes.
83+
const existingScript = document.querySelector<HTMLScriptElement>(
84+
`script[src="${src}"]`,
85+
);
86+
if (existingScript) {
87+
console.log(
88+
`drive-picker: appending a copy of an existing script ${src}`,
6989
);
70-
} else {
71-
resolve();
90+
// Copy all attributes except those that would conflict or are handled manually
91+
Array.from(existingScript.attributes).forEach((attr) => {
92+
if (["id", "src", "onload", "onerror"].includes(attr.name)) {
93+
return;
94+
}
95+
newScript.setAttribute(attr.name, attr.value);
96+
});
97+
98+
// Nonce must be copied as a property because it's not always exposed in attributes.
99+
// The nonce attribute is hidden from getAttribute() and attributes for security reasons.
100+
// It is safe to reuse the same nonce for multiple scripts on the same page (same HTTP response).
101+
if (existingScript.nonce) {
102+
newScript.nonce = existingScript.nonce;
103+
}
72104
}
105+
106+
document.head.appendChild(newScript);
73107
});
74108
}
75109

0 commit comments

Comments
 (0)