Aktualisiere Versionsnummer auf 3.2.4 in README.md, package.json, package-lock.json und package-solution.json. Füge Validierungen für sichere Formular-Elemente in CustomBrandingSettingsProvider hinzu und erweitere Tests entsprechend.

This commit is contained in:
Torsten Brendgen
2026-08-19 23:37:08 +02:00
parent 457e58b54f
commit de250e5dbb
8 changed files with 44 additions and 9 deletions
+8 -2
View File
@@ -6,7 +6,7 @@ PortalSettings V3 ist ein providerbasiertes SPFx-WebPart für SharePoint Server
| Eigenschaft | Wert | | Eigenschaft | Wert |
|---|---| |---|---|
| Version | 3.2.3 | | Version | 3.2.4 |
| SharePoint Framework | 1.4.1 | | SharePoint Framework | 1.4.1 |
| Build-Node | 8.17.0 | | Build-Node | 8.17.0 |
| npm | 6.x | | 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 - Auswahl des Navigationstermsets aus dem Default Site Collection Term Store
- Mega-Menu- oder Flyout-Modus - Mega-Menu- oder Flyout-Modus
- Cache-Dauer und Cache-Version - 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 - Debug-Modus
- schemaerhaltendes Speichern unbekannter Properties - schemaerhaltendes Speichern unbekannter Properties
@@ -165,7 +171,7 @@ sharepoint/solution/portal-settings.sppkg
## Installation ## 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. 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: 3. Die Seite mit dem PnP-PowerShell-Skript erzeugen beziehungsweise reparieren:
+1 -1
View File
@@ -2,7 +2,7 @@
Stand: 21.07.2026 Stand: 21.07.2026
Branch: `dev-3.0` Branch: `dev-3.0`
Zielversion: 3.2.3 Zielversion: 3.2.4
## Architektur ## Architektur
+1 -1
View File
@@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "portal-settings-client-side-solution", "name": "portal-settings-client-side-solution",
"id": "1c98e32d-bb7b-43b7-9625-074d6e4ea286", "id": "1c98e32d-bb7b-43b7-9625-074d6e4ea286",
"version": "3.2.3.0", "version": "3.2.4.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": false "skipFeatureDeployment": false
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "portal-settings", "name": "portal-settings",
"version": "3.2.3", "version": "3.2.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "portal-settings", "name": "portal-settings",
"version": "3.2.3", "version": "3.2.4",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "engines": {
@@ -140,7 +140,7 @@ export const CustomBrandingSettingsProvider: ISettingsProvider<ICustomBrandingSe
} }
}; };
const AllowedTags: string[] = ['div', 'span', 'p', 'a', 'button', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section']; const AllowedTags: string[] = ['div', 'span', 'p', 'a', 'button', 'form', 'input', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section'];
function validateElements(elements: any[], path: string, depth: number, state: { count: number }, errors: string[]): void { // tslint:disable-line:no-any function validateElements(elements: any[], path: string, depth: number, state: { count: number }, errors: string[]): void { // tslint:disable-line:no-any
if (depth > 8) { errors.push(path + ' überschreitet die maximale Tiefe von 8.'); return; } if (depth > 8) { errors.push(path + ' überschreitet die maximale Tiefe von 8.'); return; }
if (!Array.isArray(elements)) { errors.push(path + ' muss ein Array sein.'); 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')) { if (tag === 'img' && (!element.attributes || typeof element.attributes.alt !== 'string')) {
errors.push(path + '[' + i + '] ist ein Bild ohne alt-Attribut.'); 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') { if (element.attributes && typeof element.attributes === 'object') {
Object.keys(element.attributes).forEach((name: string): void => { Object.keys(element.attributes).forEach((name: string): void => {
const lower: string = name.toLowerCase(); const attributeValue: string = String(element.attributes[name] || ''); 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); } 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');
}
+8
View File
@@ -64,6 +64,14 @@ assert(normalizedBranding.elements === undefined, 'Legacy-Root-Elemente werden n
const unsafeBranding: any = branding.createDefault(); // tslint:disable-line:no-any 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)' } }]; 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.'); 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<any> = getSettingsProvider('expiryindicator'); // tslint:disable-line:no-any const expiry: ISettingsProvider<any> = getSettingsProvider('expiryindicator'); // tslint:disable-line:no-any
const normalizedExpiry: any = expiry.normalize({ baseField: 'Invalid field', future: true }); // tslint:disable-line:no-any const normalizedExpiry: any = expiry.normalize({ baseField: 'Invalid field', future: true }); // tslint:disable-line:no-any
+2 -2
View File
@@ -22,8 +22,8 @@ var sourceFiles = [
].map(read).join('\n'); ].map(read).join('\n');
var dataService = read('src/services/PortalSettingsDataService.ts'); var dataService = read('src/services/PortalSettingsDataService.ts');
assert(packageJson.version === '3.2.3', 'Package-Version ist nicht 3.2.3.'); assert(packageJson.version === '3.2.4', 'Package-Version ist nicht 3.2.4.');
assert(solution.version === '3.2.3.0', 'Solution-Version ist nicht 3.2.3.0.'); 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(!solution.features, 'Das inkompatible Seiten-Provisioning-Feature darf nicht paketiert werden.');
assert(!!config.bundles['portal-settings-web-part'], 'WebPart-Bundle fehlt.'); assert(!!config.bundles['portal-settings-web-part'], 'WebPart-Bundle fehlt.');
assert(copyAssets.deployCdnPath === 'temp/deploy', 'Ship-Assets werden nicht nach temp/deploy geschrieben.'); assert(copyAssets.deployCdnPath === 'temp/deploy', 'Ship-Assets werden nicht nach temp/deploy geschrieben.');