Skip to content

Commit 929bf5f

Browse files
hackall360claude
andcommitted
Security: Comprehensive security hardening
- Add DOMPurify sanitization to apps (textDemo, personaDemo, unityDemo) - All user/AI content now sanitized before innerHTML insertion - Prevents XSS attacks from malicious input or AI responses - Add Content Security Policy to all main HTML pages - Restricts script/style/font/image sources to trusted domains - Blocks clickjacking with frame-ancestors 'none' - Add Subresource Integrity (SRI) to external CDN resources - Protects against CDN tampering and supply chain attacks - Added to marked.js, DOMPurify, highlight.js - Create _headers file for security headers - X-Frame-Options: SAMEORIGIN - X-Content-Type-Options: nosniff - X-XSS-Protection: 1; mode=block - Referrer-Policy: strict-origin-when-cross-origin - Permissions-Policy for camera/microphone/geolocation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 097220e commit 929bf5f

File tree

16 files changed

+83
-26
lines changed

16 files changed

+83
-26
lines changed

_headers

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/*
2+
X-Frame-Options: SAMEORIGIN
3+
X-Content-Type-Options: nosniff
4+
X-XSS-Protection: 1; mode=block
5+
Referrer-Policy: strict-origin-when-cross-origin
6+
Permissions-Policy: camera=(), microphone=(self), geolocation=()

about/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<head>
44
<!-- Meta Tags for Cross-Browser Compatibility -->
55
<meta charset="UTF-8">
6+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://unpkg.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://image.pollinations.ai https://*.gravatar.com https://avatars.githubusercontent.com; connect-src 'self' https://text.pollinations.ai https://image.pollinations.ai https://users.unityailab.com https://api.github.com; frame-ancestors 'none';">
67
<meta http-equiv="X-UA-Compatible" content="IE=edge">
78
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes, viewport-fit=cover">
89

ai/demo/index.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8">
5+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://unpkg.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://image.pollinations.ai https://*.gravatar.com https://avatars.githubusercontent.com; connect-src 'self' https://text.pollinations.ai https://image.pollinations.ai https://users.unityailab.com https://api.github.com; frame-ancestors 'none';">
56
<meta http-equiv="X-UA-Compatible" content="IE=edge">
67
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
78

@@ -25,7 +26,7 @@
2526
<link rel="stylesheet" href="../../vendor/fontawesome/all.min.css">
2627

2728
<!-- Highlight.js for syntax highlighting -->
28-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
29+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css" integrity="sha384-oaMLBGEzBOJx3UHwac0cVndtX5fxGQIfnAeFZ35RTgqPcYlbprH9o9PUV/F8Le07" crossorigin="anonymous">
2930

3031
<!-- Main site styles -->
3132
<link rel="stylesheet" href="../../styles.css">
@@ -320,13 +321,13 @@ <h3 class="mobile-modal-title">Settings</h3>
320321
<script src="../../vendor/bootstrap/bootstrap.bundle.min.js"></script>
321322

322323
<!-- Marked.js for Markdown rendering -->
323-
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
324+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" integrity="sha384-948ahk4ZmxYVYOc+rxN1H2gM1EJ2Duhp7uHtZ4WSLkV4Vtx5MUqnV+l7u9B+jFv+" crossorigin="anonymous"></script>
324325

325326
<!-- DOMPurify for sanitization -->
326-
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
327+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js" integrity="sha384-cwS6YdhLI7XS60eoDiC+egV0qHp8zI+Cms46R0nbn8JrmoAzV9uFL60etMZhAnSu" crossorigin="anonymous"></script>
327328

328329
<!-- Highlight.js for syntax highlighting -->
329-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
330+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp" crossorigin="anonymous"></script>
330331

331332
<!-- PolliLibJS -->
332333
<!-- Only load pollylib.js for browser use - other modules are Node.js specific -->

ai/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<head>
44
<!-- Meta Tags for Cross-Browser Compatibility -->
55
<meta charset="UTF-8">
6+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://unpkg.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://image.pollinations.ai https://*.gravatar.com https://avatars.githubusercontent.com; connect-src 'self' https://text.pollinations.ai https://image.pollinations.ai https://users.unityailab.com https://api.github.com; frame-ancestors 'none';">
67
<meta http-equiv="X-UA-Compatible" content="IE=edge">
78
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes, viewport-fit=cover">
89

apps/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<head>
44
<!-- Meta Tags for Cross-Browser Compatibility -->
55
<meta charset="UTF-8">
6+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://unpkg.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://image.pollinations.ai https://*.gravatar.com https://avatars.githubusercontent.com; connect-src 'self' https://text.pollinations.ai https://image.pollinations.ai https://users.unityailab.com https://api.github.com; frame-ancestors 'none';">
67
<meta http-equiv="X-UA-Compatible" content="IE=edge">
78
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes, viewport-fit=cover">
89

apps/personaDemo/persona.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,9 @@ <h1 class="persona-title">Persona Chat</h1>
451451
<!-- Bootstrap JS -->
452452
<script src="../../vendor/bootstrap/bootstrap.bundle.min.js"></script>
453453

454+
<!-- DOMPurify for XSS Protection -->
455+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
456+
454457
<!-- Unity AI Lab Navigation -->
455458
<script src="../shared-nav.js"></script>
456459

apps/personaDemo/persona.js

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44
// Initialize PolliLibJS API
55
const polliAPI = new PollinationsAPI();
66

7+
// Sanitize HTML to prevent XSS attacks
8+
function sanitizeHTML(html) {
9+
if (typeof DOMPurify !== 'undefined') {
10+
return DOMPurify.sanitize(html, {
11+
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'b', 'i', 'u', 'a', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'img', 'span', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
12+
ALLOWED_ATTR: ['href', 'src', 'alt', 'class', 'id', 'target', 'rel', 'crossorigin', 'loading']
13+
});
14+
}
15+
return html;
16+
}
17+
718
// Settings Toggle
819
const settingsToggle = document.getElementById('settingsToggle');
920
const settingsPanel = document.getElementById('settingsPanel');
@@ -353,7 +364,7 @@ textModel.addEventListener('change', function() {
353364
role: 'system',
354365
content: modelType === 'midijourney' ? midijourneySystemPrompt : systemMessage.content
355366
}];
356-
chatOutput.innerHTML += `<p><em>Switched to ${textModel.value} model. ${modelType === 'midijourney' ? 'Starting Midijourney context...' : 'Starting new conversation with system context.'}</em></p>`;
367+
chatOutput.innerHTML += sanitizeHTML(`<p><em>Switched to ${textModel.value} model. ${modelType === 'midijourney' ? 'Starting Midijourney context...' : 'Starting new conversation with system context.'}</em></p>`);
357368
scrollToBottom();
358369
});
359370

@@ -378,7 +389,7 @@ async function generateImageFromPrompt(prompt, appendToChat = true) {
378389
const imageBlob = await response.blob();
379390
const imageObjectURL = URL.createObjectURL(imageBlob);
380391
if (appendToChat) {
381-
chatOutput.innerHTML += `<img src="${imageObjectURL}" alt="Generated Image" class="inline ${cssClass}">`;
392+
chatOutput.innerHTML += sanitizeHTML(`<img src="${imageObjectURL}" alt="Generated Image" class="inline ${cssClass}">`);
382393
scrollToBottom();
383394
}
384395
return imageObjectURL;
@@ -387,7 +398,7 @@ async function generateImageFromPrompt(prompt, appendToChat = true) {
387398
}
388399
} catch (error) {
389400
console.error("Error generating image:", error);
390-
chatOutput.innerHTML += `<p><strong>Error:</strong> Unable to generate image. Please try again.</p>`;
401+
chatOutput.innerHTML += sanitizeHTML(`<p><strong>Error:</strong> Unable to generate image. Please try again.</p>`);
391402
scrollToBottom();
392403
}
393404
}
@@ -400,7 +411,7 @@ chatForm.onsubmit = async function(event) {
400411
const selectedModel = textModel.value;
401412
const isEvil = selectedModel === 'evil';
402413
const modelType = getModelType(selectedModel);
403-
chatOutput.innerHTML += `<p><strong>${isEvil ? 'Evil User' : 'User'}:</strong> ${prompt}</p>`;
414+
chatOutput.innerHTML += sanitizeHTML(`<p><strong>${isEvil ? 'Evil User' : 'User'}:</strong> ${prompt}</p>`);
404415
userInput.value = '';
405416
scrollToBottom();
406417
if (modelType === 'chat' || isEvil) {
@@ -416,7 +427,7 @@ chatForm.onsubmit = async function(event) {
416427
messages: getModelMessages(modelType, prompt),
417428
model: selectedModel
418429
};
419-
chatOutput.innerHTML += `<p id="ai-thinking"><em>${isEvil ? 'Evil AI plotting...' : 'AI is thinking...'}</em></p>`;
430+
chatOutput.innerHTML += sanitizeHTML(`<p id="ai-thinking"><em>${isEvil ? 'Evil AI plotting...' : 'AI is thinking...'}</em></p>`);
420431
scrollToBottom();
421432
try {
422433
// Use direct fetch like demo page
@@ -447,16 +458,16 @@ chatForm.onsubmit = async function(event) {
447458
.trim();
448459
aiResponse = aiResponse.replace(/\n+/g, '\n').trim();
449460
if (aiResponse) {
450-
chatOutput.innerHTML += `<p><strong>${isEvil ? 'Evil AI' : 'AI'}:</strong> ${aiResponse}</p>`;
461+
chatOutput.innerHTML += sanitizeHTML(`<p><strong>${isEvil ? 'Evil AI' : 'AI'}:</strong> ${aiResponse}</p>`);
451462
scrollToBottom();
452463
}
453-
chatOutput.innerHTML += `<p>${isEvil ? 'Summoning evil image...' : 'Generating image...'}</p>`;
464+
chatOutput.innerHTML += sanitizeHTML(`<p>${isEvil ? 'Summoning evil image...' : 'Generating image...'}</p>`);
454465
scrollToBottom();
455466
await generateImageFromPrompt(lastImagePrompt);
456467
if (midiNotation && modelType === 'midijourney') {
457468
console.log("\n=== PLAYING MIDI SEQUENCE ===");
458469
console.log("MIDI Notation:", midiNotation);
459-
chatOutput.innerHTML += `<p>Playing musical sequence...</p>`;
470+
chatOutput.innerHTML += sanitizeHTML(`<p>Playing musical sequence...</p>`);
460471
scrollToBottom();
461472
downloadMidiBtn.disabled = false;
462473
synth.playMidiSequence(midiNotation);
@@ -467,12 +478,12 @@ chatForm.onsubmit = async function(event) {
467478
}
468479
} else if (isEvil) {
469480
aiResponse = aiResponse.replace(/\n+/g, '\n').trim();
470-
chatOutput.innerHTML += `<p><strong>Evil AI:</strong> ${aiResponse}</p>`;
471-
chatOutput.innerHTML += `<p><em>Evil AI failed to use proper image format. Next response should include ![MRKDWN]()</em></p>`;
481+
chatOutput.innerHTML += sanitizeHTML(`<p><strong>Evil AI:</strong> ${aiResponse}</p>`);
482+
chatOutput.innerHTML += sanitizeHTML(`<p><em>Evil AI failed to use proper image format. Next response should include ![MRKDWN]()</em></p>`);
472483
scrollToBottom();
473484
} else {
474485
aiResponse = aiResponse.replace(/\n+/g, '\n').trim();
475-
chatOutput.innerHTML += `<p><strong>AI:</strong> ${aiResponse}</p>`;
486+
chatOutput.innerHTML += sanitizeHTML(`<p><strong>AI:</strong> ${aiResponse}</p>`);
476487
scrollToBottom();
477488
}
478489
if (modelType === 'chat' || isEvil) {
@@ -487,7 +498,7 @@ chatForm.onsubmit = async function(event) {
487498
} catch (error) {
488499
console.error("Error:", error);
489500
const errorMessage = isEvil ? 'The darkness is temporarily unavailable. Please try again.' : 'Unable to contact AI. Please try again.';
490-
chatOutput.innerHTML += `<p><strong>Error:</strong> ${errorMessage}</p>`;
501+
chatOutput.innerHTML += sanitizeHTML(`<p><strong>Error:</strong> ${errorMessage}</p>`);
491502
scrollToBottom();
492503
const thinkingMessage = document.getElementById("ai-thinking");
493504
if (thinkingMessage) {
@@ -502,7 +513,7 @@ directImageButton.onclick = async function() {
502513
if (!prompt && !lastImagePrompt) return;
503514
const rawPrompt = prompt || lastImagePrompt;
504515
lastImagePrompt = rawPrompt;
505-
chatOutput.innerHTML += `<p>${textModel.value === 'evil' ? 'Summoning evil direct image...' : 'Generating direct image...'}</p>`;
516+
chatOutput.innerHTML += sanitizeHTML(`<p>${textModel.value === 'evil' ? 'Summoning evil direct image...' : 'Generating direct image...'}</p>`);
506517
scrollToBottom();
507518
await generateImageFromPrompt(rawPrompt);
508519
userInput.value = '';

apps/textDemo/text.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,9 @@ <h1 class="chat-title">AI Text Chat</h1>
491491
<!-- Unity AI Lab Navigation -->
492492
<script src="../shared-nav.js"></script>
493493

494+
<!-- DOMPurify for XSS Protection -->
495+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
496+
494497
<!-- PolliLibJS -->
495498
<script src="../../PolliLibJS/pollylib.js"></script>
496499

apps/textDemo/text.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44
// Initialize PolliLibJS API
55
const polliAPI = new PollinationsAPI();
66

7+
// Sanitize HTML to prevent XSS attacks
8+
function sanitizeHTML(html) {
9+
if (typeof DOMPurify !== 'undefined') {
10+
return DOMPurify.sanitize(html, {
11+
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'b', 'i', 'u', 'a', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'img', 'span', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
12+
ALLOWED_ATTR: ['href', 'src', 'alt', 'class', 'id', 'target', 'rel', 'crossorigin', 'loading', 'data-mime']
13+
});
14+
}
15+
return html;
16+
}
17+
718
const BASE_INSTRUCTIONS = `
819
I can help format code examples using [CODE] and [/CODE] tags. I will only use these tags for actual code examples.
920
When providing image URLs, please output them as plain URLs (e.g., https://image.pollinations.ai/prompt/your_prompt?params) without wrapping them in [CODE] tags so they display as images in the chat.
@@ -226,7 +237,7 @@ async function sendChatMessage(prompt, retryCount = 0) {
226237
};
227238
}
228239

229-
chatOutput.innerHTML += `<p><strong>User:</strong> ${processResponse(prompt)}</p>`;
240+
chatOutput.innerHTML += sanitizeHTML(`<p><strong>User:</strong> ${processResponse(prompt)}</p>`);
230241
scrollToBottom();
231242

232243
const thinkingElement = document.createElement('p');
@@ -280,7 +291,7 @@ async function sendChatMessage(prompt, retryCount = 0) {
280291
thinkingElem.remove();
281292
}
282293

283-
chatOutput.innerHTML += `<p><strong>AI:</strong> ${processResponse(aiResponse)}</p>`;
294+
chatOutput.innerHTML += sanitizeHTML(`<p><strong>AI:</strong> ${processResponse(aiResponse)}</p>`);
284295
scrollToBottom();
285296

286297
updateConversationHistory(prompt, aiResponse);
@@ -314,7 +325,7 @@ userInput.addEventListener('keydown', function(e) {
314325
});
315326

316327
clearChatBtn.addEventListener('click', function() {
317-
chatOutput.innerHTML = '<p>Please select a chat persona and type your message below to begin the interaction.</p>';
328+
chatOutput.innerHTML = sanitizeHTML('<p>Please select a chat persona and type your message below to begin the interaction.</p>');
318329
chatOutput.classList.add('empty');
319330
conversationHistory = [];
320331
});

apps/unityDemo/unity.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,9 @@ <h1 class="app-title">Unity Chat Interface</h1>
851851
</div>
852852
</div>
853853

854+
<!-- DOMPurify for XSS Protection -->
855+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
856+
854857
<!-- Prism.js Scripts -->
855858
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
856859
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js"></script>

0 commit comments

Comments
 (0)