XSS bei generierten Inhalten in HTML
Der alte Walter
- javascript
- php
Hallo,
ich bin online auf teils widersprüchliche Aussagen gestoßen und würde nun gerne für mehr Klarheit sorgen...
Ausgangssituation: ich weise via JavaScript FETCH requests mein PHP Script an, Daten in eine JSON zu schreiben, bzw. Daten aus der JSON Datei zu laden.
(Dies ist für meinen Usecase ausreichend, da das System im Endeffekt nur von einer Handvoll Personen genutzt wird und auch nicht öffentlich im Netz zugänglich sein wird. Im Prinzip sind daher XSS Attacken eigentlich nahezu auszuschließen – dennoch: entweder entwickle ich etwas richtig (OWASP konform) oder eben gar nicht... 🤪)
Ursprünglich sah meine Lösung vor, vor Speicherung in die JSON Datei sämtliche Inputs zu reinigen:
// PHP
function sanitizeStr(string $str): string {
return trim(htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8'));
}
Das führte erwarteterweise zu Problemen, da nun Strings im HTML zum Zeitpunkt der Darstellung nicht mehr korrekt dargestellt wurden, z.B. " Zitat 1 " & " Zitat 2 "
Darüber hinaus werden diese Zeichen aber von Input Feldern SEHR WOHL interpretiert, was zu einer Diskrepanz zwischen HTML Text und dem Text in den Input Feldern führt. Daher passen nun gewisse Input Felder zur Bearbeitung des HTML Textes nun nicht mehr zu ebendiesen.
Ich habe sanitizeStr daher aus meinem Code gestrichen.
Momentan behelfe ich mir damit, den Schritt der String-Säuberung zu überspringen, den Original Input in meiner JSON Datei zu speichern, diesen auch in Originalform abzurufen und Attacken durch Einschränkung der Browser-Interpretationsspielräume zu vermeiden, daher statt z.B.:
myForm.innerHTML = `
<input type="text" id="nameInput" name="name" value="${
data ? data.name : ""
}" autocomplete="off" required aria-required="true" aria-describedby="dataHelp"/>
`;
besser:
myForm.innerHTML = `
<input type="text" id="nameInput" name="name" autocomplete="off" required aria-required="true" aria-describedby="dataHelp"/>`;
const nameInput = crudDialogContent.querySelector("#nameInput");
nameInput.value = data?.name || "";
Dieselbe Vorgangsweise für Buttons, i.e.
const myBtn = document.querySelector("#myBtn");
myBtn.setAttribute("aria-label", label);
etc.
Momentan komme ich mir allerdings vor, als wolle ich ein Shakespeare Sonett schreiben, ohne jemals das Alphabet gelernt zu haben. 😢
Momentane Strategie: Sanitization komplett überspringen, User Inputs weder in JavaScript noch in PHP behandeln und eindeutig nicht zweideutig unmissverständliche Zuweisungen wenn innerHTML. ...Strategie richtig?
neben innerHTML weitere diesbezüglich potentiell „gefährliche“ Properties im JavaScript? (.textContent, .innerText, .type, .id, .name, .autocomplete, .required, .setAttribute, .value reagieren meines Wissens ja eher unempfindlich, bzw. sind „safe“...)
Danke für eure Hilfe!
Hi,
Ausgangssituation: ich weise via JavaScript
FETCHrequests mein PHP Script an, Daten in eine JSON zu schreiben, bzw. Daten aus der JSON Datei zu laden.Ursprünglich sah meine Lösung vor, vor Speicherung in die JSON Datei sämtliche Inputs zu reinigen:
// PHP function sanitizeStr(string $str): string { return trim(htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8')); }
falsche Methode. JSON ist nicht HTML, hier wäre Javascript-String-Behandlung nötig - sofern das nicht schon die Methode zur Erzeugung des json macht.
Die Behandlung der Zeichen für HTML (also <>& -> <>&)muß erfolgen, wenn die Daten in die HTML-Elemente geschrieben werden.
cu,
Andreas a/k/a MudGuard
falsche Methode. JSON ist nicht HTML, hier wäre Javascript-String-Behandlung nötig - sofern das nicht schon die Methode zur Erzeugung des json macht.
Soll heißen, hier besteht eigentlich kein Handlungsbedarf, Javascript-String-Behandlung sollte von
//PHP
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// ...will meine JSON Datei halbwegs leserlich gestalten
erledigt werden, richtig?
Die Behandlung der Zeichen für HTML (also
<>&-><>&)muß erfolgen, wenn die Daten in die HTML-Elemente geschrieben werden.
Ja, darüber bin ich bei meinen Recherchen bereits gestolpert. Daher:
function escapeHTML(str) {
return str.replace(/[&<>"']/g, function (char) {
switch (char) {
case "&":
return "&";
case "<":
return "<";
case ">":
return ">";
case '"':
return """;
case "'":
return "'";
}
});
}
und dann
myForm.innerHTML = `
<input type="text" id="nameInput" name="name" value="${
data ? escapeHTML(data.name) : ""
}" autocomplete="off" required aria-required="true" aria-describedby="dataHelp"/>
`;
❓
cu,
Andreas a/k/a MudGuard
Danke Dir für Deine Hilfe! 😀😃👍👍
@@Der alte Walter
Das führte erwarteterweise zu Problemen, da nun Strings im HTML zum Zeitpunkt der Darstellung nicht mehr korrekt dargestellt wurden, z.B.
" Zitat 1 " & " Zitat 2 "
Was auch an deinen Daten liegt. Mit " ist die Darstellung nicht korrekt; korrekt ist „Zitat 1“ & „Zitat 2“ – wenn der Inhalt auf deutsch ist. Im Englischen sehen Anführungszeichen so aus: “”.
Momentan komme ich mir allerdings vor, als wolle ich ein Shakespeare Sonett schreiben, ohne jemals das Alphabet gelernt zu haben. 😢
Das schaffen sogar Affen.
🖖 Live long and prosper
Hallo Walter,
Um das noch mal zu bündeln: die JSON Funktionen in PHP kümmern sich darum, beliebige Strings sauber in JSON zu verpacken, und die JSON Funktionen in Javascript können das auch. Sogar kompatibel zu dem, was PHP tut.
Dass du die nötigen Elemente erstmal ohne Daten erzeugst, sie dann per id heraussuchst und die Daten an die entsprechenden Eigenschaften oder Attribute zuweist, ist einer von mehreren richtigen Wegen.
Sicherlich wird dein Form mehrere Elemente enthalten, dazu kommen noch die obligatorischen Label-Elemente, da erstellt man das Gerüst gerne auf diese Weise. Eine Alternative wäre ein template-Element.
Die Daten in den HTML String einzubauen ist jedenfalls mühsamer und fehlerträchtiger. Es geht, natürlich geht es, aber du musst es selbst tun. Der Trick dazu ist ein Dummy-Element, das man nicht ins DOM hängt. Man weist den Text an die textContent-Eigenschaft zu und liest ihn aus innerHTML wieder aus. Zack, fertig maskiert. Aber nicht der beste Weg.
Rolf
Die Daten in den HTML String einzubauen ist jedenfalls mühsamer und fehlerträchtiger. Es geht, natürlich geht es, aber du musst es selbst tun. Der Trick dazu ist ein Dummy-Element, das man nicht ins DOM hängt. Man weist den Text an die textContent-Eigenschaft zu und liest ihn aus innerHTML wieder aus. Zack, fertig maskiert. Aber nicht der beste Weg.
Wäre das so eine saubere Lösung deiner Meinung nach?:
function htmlEncode(str) {
const div = document.createElement('div'); // oder irgendein anderes Element
div.textContent = str;
return div.innerHTML;
}
MudGuard scheint anderer Meinung zu sein (siehe oben), wobei mir auch diese Lösung gangbar erscheint:
Die Behandlung der Zeichen für HTML (also <>& -> <>&)muß erfolgen, wenn die Daten in die HTML-Elemente geschrieben werden.
Bin da diesbezüglich schon vor meinem ersten Post auf folgendes gestoßen (siehe Konversation oben):
function escapeHTML(str) {
return str.replace(/[&<>"']/g, function (char) {
switch (char) {
case "&":
return "&";
case "<":
return "<";
case ">":
return ">";
case '"':
return """;
case "'":
return "'";
}
});
}
Beides akzeptabel? Eine Version besser als die andere? JA WAS DENN NU? 😂
Rolf
Vielen lieben Dank für Deine Hilfe! 😃👍