diff --git a/README.md b/README.md index 4fc114c..39ba628 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ PortalSettings V3 ist ein providerbasiertes SPFx-WebPart für SharePoint Server | Eigenschaft | Wert | |---|---| -| Version | 3.2.3 | +| Version | 3.2.4 | | SharePoint Framework | 1.4.1 | | Build-Node | 8.17.0 | | npm | 6.x | @@ -23,6 +23,12 @@ Die Oberfläche wird vollständig vom WebPart erzeugt und verwendet kein `innerH - Auswahl des Navigationstermsets aus dem Default Site Collection Term Store - Mega-Menu- oder Flyout-Modus - Cache-Dauer und Cache-Version + +### Custom Branding + +- sichere Header- und Footer-Elemente sowie Stylesheets +- deklarative GET-Suchformulare über die erlaubten Elementtypen `form` und `input` +- Prüfung von Formular-Action, Eingabetyp und Submit-Button vor dem Speichern - Debug-Modus - schemaerhaltendes Speichern unbekannter Properties @@ -165,7 +171,7 @@ sharepoint/solution/portal-settings.sppkg ## Installation -1. `portal-settings.sppkg` im App Catalog durch Version `3.2.3.0` ersetzen. +1. `portal-settings.sppkg` im App Catalog durch Version `3.2.4.0` ersetzen. 2. Die App im Root Web der gewünschten Site Collection installieren oder aktualisieren. 3. Die Seite mit dem PnP-PowerShell-Skript erzeugen beziehungsweise reparieren: diff --git a/ToDo.md b/ToDo.md index 95b6cf6..e6f4642 100644 --- a/ToDo.md +++ b/ToDo.md @@ -2,7 +2,7 @@ Stand: 21.07.2026 Branch: `dev-3.0` -Zielversion: 3.2.3 +Zielversion: 3.2.4 ## Architektur diff --git a/config/package-solution.json b/config/package-solution.json index 233b697..e686b37 100644 --- a/config/package-solution.json +++ b/config/package-solution.json @@ -3,7 +3,7 @@ "solution": { "name": "portal-settings-client-side-solution", "id": "1c98e32d-bb7b-43b7-9625-074d6e4ea286", - "version": "3.2.3.0", + "version": "3.2.4.0", "includeClientSideAssets": true, "skipFeatureDeployment": false }, diff --git a/package-lock.json b/package-lock.json index 7335177..d708da4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "portal-settings", - "version": "3.2.3", + "version": "3.2.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index ffbdad4..6e249a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "portal-settings", - "version": "3.2.3", + "version": "3.2.4", "private": true, "main": "lib/index.js", "engines": { diff --git a/src/providers/CustomBrandingSettingsProvider.ts b/src/providers/CustomBrandingSettingsProvider.ts index 5ab7e4c..fd44e6b 100644 --- a/src/providers/CustomBrandingSettingsProvider.ts +++ b/src/providers/CustomBrandingSettingsProvider.ts @@ -140,7 +140,7 @@ export const CustomBrandingSettingsProvider: ISettingsProvider 8) { errors.push(path + ' überschreitet die maximale Tiefe von 8.'); return; } if (!Array.isArray(elements)) { errors.push(path + ' muss ein Array sein.'); return; } @@ -154,6 +154,21 @@ function validateElements(elements: any[], path: string, depth: number, state: { if (tag === 'img' && (!element.attributes || typeof element.attributes.alt !== 'string')) { errors.push(path + '[' + i + '] ist ein Bild ohne alt-Attribut.'); } + if (tag === 'form') { + const action: string = String(element.attributes && element.attributes.action || '').trim(); + if (!action || !isSafeFormAction(action)) { errors.push(path + '[' + i + '] besitzt keine sichere Formular-Action.'); } + if (element.attributes && element.attributes.method && String(element.attributes.method).toLowerCase() !== 'get') { + errors.push(path + '[' + i + '] darf nur die Formularmethode GET verwenden.'); + } + } + if (tag === 'input' && element.attributes && element.attributes.type && + String(element.attributes.type).toLowerCase() !== 'search') { + errors.push(path + '[' + i + '] darf nur den Eingabetyp search verwenden.'); + } + if (tag === 'button' && element.attributes && element.attributes.type && + ['button', 'submit'].indexOf(String(element.attributes.type).toLowerCase()) < 0) { + errors.push(path + '[' + i + '] verwendet einen nicht erlaubten Button-Typ.'); + } if (element.attributes && typeof element.attributes === 'object') { Object.keys(element.attributes).forEach((name: string): void => { const lower: string = name.toLowerCase(); const attributeValue: string = String(element.attributes[name] || ''); @@ -170,3 +185,9 @@ function validateElements(elements: any[], path: string, depth: number, state: { if (element.children) { validateElements(element.children, path + '[' + i + '].children', depth + 1, state, errors); } } } + +function isSafeFormAction(value: string): boolean { + if (/^~sitecollection(?:\/|$)/i.test(value) || /^\/(?!\/)/.test(value)) { return true; } + const protocol: RegExpMatchArray = value.match(/^([a-z][a-z0-9+.-]*):/i); + return !!protocol && (protocol[1].toLowerCase() === 'http' || protocol[1].toLowerCase() === 'https'); +} diff --git a/tests/ProviderRegistry.test.ts b/tests/ProviderRegistry.test.ts index 344cc1c..3341918 100644 --- a/tests/ProviderRegistry.test.ts +++ b/tests/ProviderRegistry.test.ts @@ -64,6 +64,14 @@ assert(normalizedBranding.elements === undefined, 'Legacy-Root-Elemente werden n const unsafeBranding: any = branding.createDefault(); // tslint:disable-line:no-any unsafeBranding.placeholdertop.elements = [{ type: 'script', content: 'alert(1)' }, { type: 'a', attributes: { onclick: 'alert(1)', href: 'javascript:alert(1)' } }]; assert(branding.validate(unsafeBranding).length >= 3, 'Unsichere Branding-Elemente werden nicht erkannt.'); +const searchBranding: any = branding.createDefault(); // tslint:disable-line:no-any +searchBranding.placeholdertop.elements = [{ + type: 'form', attributes: { action: '~sitecollection/_layouts/15/search.aspx/siteall', method: 'get' }, + children: [{ type: 'input', attributes: { type: 'search', name: 'q' } }] +}]; +assert(branding.validate(searchBranding).length === 0, 'Sichere deklarative Suchformulare werden nicht akzeptiert.'); +searchBranding.placeholdertop.elements[0].attributes.action = 'javascript:alert(1)'; +assert(branding.validate(searchBranding).length > 0, 'Unsichere Formular-Actions werden nicht erkannt.'); const expiry: ISettingsProvider = getSettingsProvider('expiryindicator'); // tslint:disable-line:no-any const normalizedExpiry: any = expiry.normalize({ baseField: 'Invalid field', future: true }); // tslint:disable-line:no-any diff --git a/tests/validate-static-assets.js b/tests/validate-static-assets.js index 4e1542d..7cebec3 100644 --- a/tests/validate-static-assets.js +++ b/tests/validate-static-assets.js @@ -22,8 +22,8 @@ var sourceFiles = [ ].map(read).join('\n'); var dataService = read('src/services/PortalSettingsDataService.ts'); -assert(packageJson.version === '3.2.3', 'Package-Version ist nicht 3.2.3.'); -assert(solution.version === '3.2.3.0', 'Solution-Version ist nicht 3.2.3.0.'); +assert(packageJson.version === '3.2.4', 'Package-Version ist nicht 3.2.4.'); +assert(solution.version === '3.2.4.0', 'Solution-Version ist nicht 3.2.4.0.'); assert(!solution.features, 'Das inkompatible Seiten-Provisioning-Feature darf nicht paketiert werden.'); assert(!!config.bundles['portal-settings-web-part'], 'WebPart-Bundle fehlt.'); assert(copyAssets.deployCdnPath === 'temp/deploy', 'Ship-Assets werden nicht nach temp/deploy geschrieben.');