Skip to content

Commit 2667840

Browse files
claudehyperpolymath
authored andcommitted
Fix CodeQL security warnings
- Fix Flask debug mode: Use environment variable FLASK_DEBUG instead of hardcoded debug=True to prevent running in debug mode in production - Fix unsafe HTML construction in JavaScript client library: - Replace innerHTML with safe DOM API methods (createElement, textContent) - Add sanitizeUrl helper to prevent javascript:/data: protocol XSS attacks - Build SVG elements using createElementNS for safer rendering - Add explicit permissions to GitHub Actions workflows: - test.yml: contents: read - security.yml: contents: read (workflow-level), issues: write (security-summary job) - build.yml: contents: read - rsr-compliance.yml: contents: read - ci.yml: contents: read - docs.yml: contents: write, pages: write, id-token: write (for gh-deploy) These changes follow the principle of least privilege and address all CodeQL security warnings for Flask debug mode, unsafe HTML construction, and missing workflow permissions.
1 parent 22c0c28 commit 2667840

File tree

8 files changed

+166
-26
lines changed

8 files changed

+166
-26
lines changed

.github/workflows/build.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ on:
1111
release:
1212
types: [published]
1313

14+
# Default permissions for all jobs (principle of least privilege)
15+
permissions:
16+
contents: read
17+
1418
jobs:
1519
build-styles:
1620
name: SCSS → CSS Compilation

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ on:
55
pull_request:
66
branches: [ main ]
77

8+
# Default permissions for all jobs (principle of least privilege)
9+
permissions:
10+
contents: read
11+
812
jobs:
913
lint:
1014
runs-on: ubuntu-latest

.github/workflows/docs.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ on:
55
branches:
66
- main # Deploy on pushes to the main branch
77

8+
# Permissions for GitHub Pages deployment
9+
permissions:
10+
contents: write # Required for gh-deploy to push to gh-pages branch
11+
pages: write # Required for GitHub Pages deployment
12+
id-token: write # Required for GitHub Pages deployment
13+
814
jobs:
915
deploy:
1016
runs-on: ubuntu-latest

.github/workflows/rsr-compliance.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ on:
99
push:
1010
branches: [ main ]
1111

12+
# Default permissions for all jobs (principle of least privilege)
13+
permissions:
14+
contents: read
15+
1216
jobs:
1317
rsr-core-check:
1418
name: RSR Core Files Check

.github/workflows/security.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ on:
1212
# Run daily at 02:00 UTC
1313
- cron: '0 2 * * *'
1414

15+
# Default permissions for all jobs (principle of least privilege)
16+
permissions:
17+
contents: read
18+
1519
jobs:
1620
npm-audit:
1721
name: NPM Security Audit
@@ -240,6 +244,8 @@ jobs:
240244
runs-on: ubuntu-latest
241245
needs: [npm-audit, codeql-analysis, secret-scanning, security-txt-validation, license-compliance, vulnerability-disclosure-policy]
242246
if: always()
247+
permissions:
248+
issues: write # Required for creating security alert issues
243249
steps:
244250
- name: Check security results
245251
run: |

.github/workflows/test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ on:
99
push:
1010
branches: [ main ]
1111

12+
# Default permissions for all jobs (principle of least privilege)
13+
permissions:
14+
contents: read
15+
1216
jobs:
1317
test-haskell:
1418
name: Haskell Validator Tests

integrations/client/javascript/src/index.js

Lines changed: 132 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,43 @@ const DEFAULT_CONFIG = {
2020
theme: 'light'
2121
};
2222

23+
/**
24+
* Sanitize a URL to prevent XSS via javascript: or data: protocols
25+
*
26+
* @param {string} url - The URL to sanitize
27+
* @returns {string} Sanitized URL or empty string if invalid
28+
*/
29+
function sanitizeUrl(url) {
30+
if (typeof url !== 'string') {
31+
return '';
32+
}
33+
34+
const trimmedUrl = url.trim().toLowerCase();
35+
36+
// Block dangerous protocols
37+
if (trimmedUrl.startsWith('javascript:') ||
38+
trimmedUrl.startsWith('data:') ||
39+
trimmedUrl.startsWith('vbscript:')) {
40+
return '';
41+
}
42+
43+
// Only allow http, https, and relative URLs
44+
if (trimmedUrl.startsWith('http://') ||
45+
trimmedUrl.startsWith('https://') ||
46+
trimmedUrl.startsWith('/') ||
47+
trimmedUrl.startsWith('./') ||
48+
trimmedUrl.startsWith('../')) {
49+
return url;
50+
}
51+
52+
// For other cases, prepend https:// if it looks like a domain
53+
if (trimmedUrl.match(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z]{2,})+/)) {
54+
return 'https://' + url;
55+
}
56+
57+
return '';
58+
}
59+
2360
/**
2461
* Extract license metadata from the current page
2562
*
@@ -147,7 +184,8 @@ export function createLicenseBadge(options = {}) {
147184
const config = { ...DEFAULT_CONFIG, ...options };
148185
const badge = document.createElement('a');
149186

150-
badge.href = config.licenseUrl;
187+
// Sanitize URL to prevent XSS via javascript: protocol
188+
badge.href = sanitizeUrl(config.licenseUrl);
151189
badge.target = '_blank';
152190
badge.rel = 'license noopener noreferrer';
153191
badge.className = 'palimpsest-badge';
@@ -182,14 +220,33 @@ export function createLicenseBadge(options = {}) {
182220
? `Palimpsest Licentie v${config.version}`
183221
: `Palimpsest License v${config.version}`;
184222

185-
badge.innerHTML = `
186-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right: 6px;">
187-
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2"/>
188-
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2"/>
189-
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2"/>
190-
</svg>
191-
${text}
192-
`;
223+
// Create SVG element safely
224+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
225+
svg.setAttribute('width', '16');
226+
svg.setAttribute('height', '16');
227+
svg.setAttribute('viewBox', '0 0 24 24');
228+
svg.setAttribute('fill', 'none');
229+
svg.style.marginRight = '6px';
230+
231+
const paths = [
232+
'M12 2L2 7L12 12L22 7L12 2Z',
233+
'M2 17L12 22L22 17',
234+
'M2 12L12 17L22 12'
235+
];
236+
237+
paths.forEach(d => {
238+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
239+
path.setAttribute('d', d);
240+
path.setAttribute('stroke', 'currentColor');
241+
path.setAttribute('stroke-width', '2');
242+
svg.appendChild(path);
243+
});
244+
245+
badge.appendChild(svg);
246+
247+
// Create text node safely (prevents XSS)
248+
const textNode = document.createTextNode(text);
249+
badge.appendChild(textNode);
193250

194251
badge.addEventListener('mouseenter', () => {
195252
badge.style.borderColor = config.theme === 'dark' ? '#58a6ff' : '#0366d6';
@@ -240,29 +297,79 @@ export function createLicenseNotice(options = {}) {
240297
`;
241298

242299
const text = config.language === 'nl' ? {
243-
main: `Dit werk is beschermd onder de <strong>Palimpsest Licentie v${config.version}</strong>.`,
300+
mainPrefix: 'Dit werk is beschermd onder de ',
301+
mainLicense: `Palimpsest Licentie v${config.version}`,
302+
mainSuffix: '.',
244303
requirement: 'Afgeleiden moeten de emotionele en culturele integriteit van het origineel behouden.',
245304
link: 'Lees de volledige licentie'
246305
} : {
247-
main: `This work is protected under the <strong>Palimpsest License v${config.version}</strong>.`,
306+
mainPrefix: 'This work is protected under the ',
307+
mainLicense: `Palimpsest License v${config.version}`,
308+
mainSuffix: '.',
248309
requirement: "Derivatives must preserve the original's emotional and cultural integrity.",
249310
link: 'Read the full license'
250311
};
251312

252-
notice.innerHTML = `
253-
<div style="display: flex; align-items: center;">
254-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" style="margin-right: 12px; flex-shrink: 0;">
255-
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="${styles.linkColor}" stroke-width="2"/>
256-
<path d="M2 17L12 22L22 17" stroke="${styles.linkColor}" stroke-width="2"/>
257-
<path d="M2 12L12 17L22 12" stroke="${styles.linkColor}" stroke-width="2"/>
258-
</svg>
259-
<div>
260-
<p style="margin: 0 0 8px 0;">${text.main}</p>
261-
<p style="margin: 0 0 8px 0; font-size: 13px; opacity: 0.8;">${text.requirement}</p>
262-
<a href="${config.licenseUrl}" style="color: ${styles.linkColor}; text-decoration: none; font-weight: 500;" target="_blank" rel="license noopener">${text.link} →</a>
263-
</div>
264-
</div>
265-
`;
313+
// Build DOM safely to prevent XSS
314+
const container = document.createElement('div');
315+
container.style.cssText = 'display: flex; align-items: center;';
316+
317+
// Create SVG safely
318+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
319+
svg.setAttribute('width', '24');
320+
svg.setAttribute('height', '24');
321+
svg.setAttribute('viewBox', '0 0 24 24');
322+
svg.setAttribute('fill', 'none');
323+
svg.style.cssText = 'margin-right: 12px; flex-shrink: 0;';
324+
325+
const pathData = [
326+
'M12 2L2 7L12 12L22 7L12 2Z',
327+
'M2 17L12 22L22 17',
328+
'M2 12L12 17L22 12'
329+
];
330+
331+
pathData.forEach(d => {
332+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
333+
path.setAttribute('d', d);
334+
path.setAttribute('stroke', styles.linkColor);
335+
path.setAttribute('stroke-width', '2');
336+
svg.appendChild(path);
337+
});
338+
339+
container.appendChild(svg);
340+
341+
// Create content div
342+
const contentDiv = document.createElement('div');
343+
344+
// Main paragraph with safe text content
345+
const mainP = document.createElement('p');
346+
mainP.style.cssText = 'margin: 0 0 8px 0;';
347+
mainP.appendChild(document.createTextNode(text.mainPrefix));
348+
const strong = document.createElement('strong');
349+
strong.textContent = text.mainLicense;
350+
mainP.appendChild(strong);
351+
mainP.appendChild(document.createTextNode(text.mainSuffix));
352+
353+
// Requirement paragraph
354+
const reqP = document.createElement('p');
355+
reqP.style.cssText = 'margin: 0 0 8px 0; font-size: 13px; opacity: 0.8;';
356+
reqP.textContent = text.requirement;
357+
358+
// Link with URL validation
359+
const link = document.createElement('a');
360+
// Validate URL to prevent javascript: protocol injection
361+
const sanitizedUrl = sanitizeUrl(config.licenseUrl);
362+
link.href = sanitizedUrl;
363+
link.style.cssText = `color: ${styles.linkColor}; text-decoration: none; font-weight: 500;`;
364+
link.target = '_blank';
365+
link.rel = 'license noopener';
366+
link.textContent = text.link + ' →';
367+
368+
contentDiv.appendChild(mainP);
369+
contentDiv.appendChild(reqP);
370+
contentDiv.appendChild(link);
371+
container.appendChild(contentDiv);
372+
notice.appendChild(container);
266373

267374
return notice;
268375
}

integrations/server/python/examples/basic_flask.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,13 @@ def api_stories():
167167

168168

169169
if __name__ == "__main__":
170+
import os
171+
170172
print("Starting Flask application with Palimpsest License integration")
171173
print("Palimpsest License middleware active")
172174
print("Visit http://localhost:5000")
173175
print("License info available at http://localhost:5000/license.json")
174-
app.run(debug=True, port=5000)
176+
177+
# Use environment variable for debug mode (default to False for security)
178+
debug_mode = os.environ.get("FLASK_DEBUG", "false").lower() == "true"
179+
app.run(debug=debug_mode, port=5000)

0 commit comments

Comments
 (0)