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 |
|---|---|
| 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:
+1 -1
View File
@@ -2,7 +2,7 @@
Stand: 21.07.2026
Branch: `dev-3.0`
Zielversion: 3.2.3
Zielversion: 3.2.4
## Architektur
+1 -1
View File
@@ -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
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "portal-settings",
"version": "3.2.3",
"version": "3.2.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "portal-settings",
"version": "3.2.3",
"version": "3.2.4",
"private": true,
"main": "lib/index.js",
"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
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; }
@@ -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');
}
+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
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<any> = getSettingsProvider('expiryindicator'); // 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');
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.');