diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd7d262..8f660df 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,10 @@ on: release: types: [published] +# Default permissions for all jobs (principle of least privilege) +permissions: + contents: read + jobs: build-styles: name: SCSS → CSS Compilation diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31da6a3..2fcd46d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,10 @@ on: pull_request: branches: [ main ] +# Default permissions for all jobs (principle of least privilege) +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7e2dfcb..e6777d5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,6 +5,12 @@ on: branches: - main # Deploy on pushes to the main branch +# Permissions for GitHub Pages deployment +permissions: + contents: write # Required for gh-deploy to push to gh-pages branch + pages: write # Required for GitHub Pages deployment + id-token: write # Required for GitHub Pages deployment + jobs: deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/rsr-compliance.yml b/.github/workflows/rsr-compliance.yml index b89247e..21eee58 100644 --- a/.github/workflows/rsr-compliance.yml +++ b/.github/workflows/rsr-compliance.yml @@ -9,6 +9,10 @@ on: push: branches: [ main ] +# Default permissions for all jobs (principle of least privilege) +permissions: + contents: read + jobs: rsr-core-check: name: RSR Core Files Check diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index fa9ca8a..f7a56b6 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -12,6 +12,10 @@ on: # Run daily at 02:00 UTC - cron: '0 2 * * *' +# Default permissions for all jobs (principle of least privilege) +permissions: + contents: read + jobs: npm-audit: name: NPM Security Audit @@ -240,6 +244,8 @@ jobs: runs-on: ubuntu-latest needs: [npm-audit, codeql-analysis, secret-scanning, security-txt-validation, license-compliance, vulnerability-disclosure-policy] if: always() + permissions: + issues: write # Required for creating security alert issues steps: - name: Check security results run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4dcf661..f93445e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,6 +9,10 @@ on: push: branches: [ main ] +# Default permissions for all jobs (principle of least privilege) +permissions: + contents: read + jobs: test-haskell: name: Haskell Validator Tests diff --git a/integrations/client/javascript/src/index.js b/integrations/client/javascript/src/index.js index 348b09d..c8b51c0 100644 --- a/integrations/client/javascript/src/index.js +++ b/integrations/client/javascript/src/index.js @@ -20,6 +20,43 @@ const DEFAULT_CONFIG = { theme: 'light' }; +/** + * Sanitize a URL to prevent XSS via javascript: or data: protocols + * + * @param {string} url - The URL to sanitize + * @returns {string} Sanitized URL or empty string if invalid + */ +function sanitizeUrl(url) { + if (typeof url !== 'string') { + return ''; + } + + const trimmedUrl = url.trim().toLowerCase(); + + // Block dangerous protocols + if (trimmedUrl.startsWith('javascript:') || + trimmedUrl.startsWith('data:') || + trimmedUrl.startsWith('vbscript:')) { + return ''; + } + + // Only allow http, https, and relative URLs + if (trimmedUrl.startsWith('http://') || + trimmedUrl.startsWith('https://') || + trimmedUrl.startsWith('/') || + trimmedUrl.startsWith('./') || + trimmedUrl.startsWith('../')) { + return url; + } + + // For other cases, prepend https:// if it looks like a domain + if (trimmedUrl.match(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z]{2,})+/)) { + return 'https://' + url; + } + + return ''; +} + /** * Extract license metadata from the current page * @@ -147,7 +184,8 @@ export function createLicenseBadge(options = {}) { const config = { ...DEFAULT_CONFIG, ...options }; const badge = document.createElement('a'); - badge.href = config.licenseUrl; + // Sanitize URL to prevent XSS via javascript: protocol + badge.href = sanitizeUrl(config.licenseUrl); badge.target = '_blank'; badge.rel = 'license noopener noreferrer'; badge.className = 'palimpsest-badge'; @@ -182,14 +220,33 @@ export function createLicenseBadge(options = {}) { ? `Palimpsest Licentie v${config.version}` : `Palimpsest License v${config.version}`; - badge.innerHTML = ` - - ${text} - `; + // Create SVG element safely + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', '16'); + svg.setAttribute('height', '16'); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('fill', 'none'); + svg.style.marginRight = '6px'; + + const paths = [ + 'M12 2L2 7L12 12L22 7L12 2Z', + 'M2 17L12 22L22 17', + 'M2 12L12 17L22 12' + ]; + + paths.forEach(d => { + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', d); + path.setAttribute('stroke', 'currentColor'); + path.setAttribute('stroke-width', '2'); + svg.appendChild(path); + }); + + badge.appendChild(svg); + + // Create text node safely (prevents XSS) + const textNode = document.createTextNode(text); + badge.appendChild(textNode); badge.addEventListener('mouseenter', () => { badge.style.borderColor = config.theme === 'dark' ? '#58a6ff' : '#0366d6'; @@ -240,29 +297,79 @@ export function createLicenseNotice(options = {}) { `; const text = config.language === 'nl' ? { - main: `Dit werk is beschermd onder de Palimpsest Licentie v${config.version}.`, + mainPrefix: 'Dit werk is beschermd onder de ', + mainLicense: `Palimpsest Licentie v${config.version}`, + mainSuffix: '.', requirement: 'Afgeleiden moeten de emotionele en culturele integriteit van het origineel behouden.', link: 'Lees de volledige licentie' } : { - main: `This work is protected under the Palimpsest License v${config.version}.`, + mainPrefix: 'This work is protected under the ', + mainLicense: `Palimpsest License v${config.version}`, + mainSuffix: '.', requirement: "Derivatives must preserve the original's emotional and cultural integrity.", link: 'Read the full license' }; - notice.innerHTML = ` -