JavaScript NodeJS, sse, proxy und sicherheit

mercsen

Lt. Commander
Registriert
Apr. 2010
Beiträge
1.680
Moin liebe CB gemeinde,

wieder würde ich gerne eure meinung zu einem problem und meinen ideen dazu hören :)

wie einige sicher mtibekommen haben arbeite ich an einer projektverwaltungs software und der versuche ich nun beizubringen die eingaben anderer user direkt auf dem bildschirm anzuzeigen.

Ich habe das nun mit einem NodeJS server und SSE gelöst.
Ein user betritt also die seite, denn wird eine EventSource geöffnet (der nodejs server) und der schickt, sobald er von einem anderen user die meldung einer änderung erhält, an alle clienten diese änderung und das clientseitige JavaScript baut es ein.

das klappt alles wunderbar (nochmal danke an die helfer hier :)), doch natürlich darf nicht einfach jeder diese daten geschickt bekommen. Es werden zwar (noch) keine sicherheitsrelevatnten daten verschickt, aber ich möchte trotzdem sicher gehen das nur eingeloggte user die daten bekommen.

ich habe mir mal angeschaut was JS an den NodeJs server sendet, er schickt dort immer die PHPSESSIONID mit, damit könnte ich ihn also theoretisch identifizieren.

Ich habe mir das so überlegt:
Ein user loggt sich ein, dann sendet php an den NodeJs server die sessionid und ab dann weiß er das ein client berehctigt ist daten zu empfangen.
Problem: man könnte die session einfach übernehmen.

dann war meine zwiete überlegung die sessionid an eine IP zu koppeln, doch da stellt sich mir ein problem in den weg, diese verdammte same origin policy!

Damit die EventSource auf meinen NodeJS server zugreifen kann (der auf port 8765 lauscht) muss ich die verbindung durch einen proxy jagen, das geschieht hier in form des moduls mod_proxy von apache.

Ich habe einfach eine .htaccess datei erstellt die alle anfragen an /see/ auf port 8765 weiterleitet.
So denkt JavaScript alles spelt sich auf dem selben host+port ab.
die htaccess
Code:
RewriteRule ^(sse)/$ https://xxx.de:8765/ [P]

durch dieses weiterleiten wird die remoteip der request, die am nodejs server eingehen, aber automatisch zur ip des servers (also quasi localhost).
So kann ich die session dann nicht mehr mit einer IP abgleichen.

Alternativ würde es wohl auch reichen, bei einem verbundungsaufbau einmal die daten mitzuschicken, denn eigentlich sollte die verbindung ja stets aufrecht bleiben, zumal ich in meinen Events, die der nodejs server erzeugt, im header immer ein keep-alive mitschicke.
Reicht das?

Was könnte man sonst machen?

Könnte ich in der htaccess datei dafür sorgen, das die richtige remote IP mitgeschickt wird?

Oder bin ich ganz und gar auf dem Holzweg? Für denk anstöße und diskussionen wäre ich dankbar :)

edit:
jajajajajajajaaaaaaaaaaaaaa wieder zu schnell.
Man kommt doch an die richtige IP adresse, im reuqest.header gibt es den eintrag x-forwarded-for, mit der richtigen remoteip.

Dennoch meine frage ob es überhaupt sinnvoll ist

reicht es evtl. schon nur auf die ip zu pürfen? wenn die verbindung abreist kann man diese ja wieder löschen?
oder sehe ich das ein wenig zu einfach?
 
Zuletzt bearbeitet:
Also ich mache mir das ganze eine Ecke leichter:
Meine Seite wird über PHP generiert und kommuniziert kaum mit dem Node.js Server.
Ich habe dann definiert, welche Infos der Node.js braucht um Nachrichten zustellen zu können:
  • User-ID
  • ggf. Channel-ID (falls man nur auf bestimmte Events hören möchte)

Und wie der Node.js den User authentifiziert:
  • User-ID
  • IP-Adresse
  • Browserkennung (Hash)


Die PHP-Seite erzeugt dann folgendermaßen einen Hash:
Code:
hmac_sha512(geheimes Passwort, UserId + ChannelId + IP-Adresse + Browserkennung + Nonce + Serverzeit)
Der Client wird dann beim Node.js mit dem Hash und dem Senden dieser Infos versuchen eine SSE-Verbindung aufzubauen. Der Nodejs-Server wird die Infos ebenfalls genauso hashen und mit den Daten der Clientverbindung vergleichen (IP-Adresse, Browserkennung). Sind die Daten identisch, ist schonmal relativ sicher, dass es der gleiche Client ist.

Die Nonce hat einfach den Zweck sicherzustellen, dass die erzeugten Hashes immer unterschiedlich sind.

Die Serverzeit spielt eine wichtige Rolle: Wenn ein PHP-Script diesen Hash erstellt soll er nur 60s gültig sein. Indem die Serverzeit mitgesendet wird, kann der Nodejs-Server prüfen ob die Anfrage ok ist (und keine alte generierte geklaute URL). Ist sie korrekt wird der ganze Hash für die nächsten 60s innerhalb des Nodejs-Servers auf eine Blacklist gesetzt, es ist also nur einmal möglich mit der URL zu verbinden.

Das Hashen ist nötig, damit man sicher sein kann, dass die Daten nicht vom Client gefälscht werden.
Der ganze Aufbau hat einen Zweck: Der PHP- und Node.js Server müssen niemals darüber kommunizieren ob eine SSE-Verbindung zugelassen werden darf, es kann einfach lokal auf Basis der gesendeten Informationen vom Nodejs-Server berechnet werden. Der PHP-Server muss einfach nur noch als "dumme" Anwendung jedes Event an den NodeJS-Server senden und dieser kümmert sich dann darum die Nachricht zuzustellen, wenn ein Client verbunden ist oder verwirft sie.

Das ganze sieht auf den ersten Blick ziemlich komplex aus, aber erfüllt einen gewissen Grad an Sicherheit und ist "genial", denn beide Systeme müssen gar nicht mehr miteinander kommunizieren. Die Idee dafür hatte ich mal in einer IT-Sicherheitsvorlesung ^^
 
Zuletzt bearbeitet:
dakann ich dir grad net so ganz folgen....

channelID wegen events?
Ich baller alle Events über eine verbindung raus und defeniere über event: abc dann was für ein event das ist. An die EventSource habe ich dann demenstürechend einen handler für event abc angehängt.

und das mit dem senden ist mir grad auch nicht ganz geläufig.
PHP erstellt jetzt den hash, aber wie kommt der auf den node server wenn die nicht kommunizieren?
Odersoll der von php erzeugte hash beim connecten mitgesendet werden, zzgl. den einzelnen infos der hash und dann soll der neu gebaut und verglichen werden?
Was würde mich dann davon abhalten, mit dem wissen wie der hash gebaut wird, einen eigenen fake hash mitzuschiken der dann von Node verifiziert wird?
Oder verstehe ich das ganz falsch?
Ist das nicht verschlüßeln durch verschleiern?

Leider werde ich am ende das ganze ding als Open Source veröffentlichen, also käme sowas net in frage :-/

Zudem soll es möglich sein sich mehrmal zu verbinden, da meine user gerne mit 2 Browserfenster gleichzeitig arbeiten. Bzw. alle User aus einem Büro natürlich die selbe IP haben. (vorhin vergessen zu erwähnen)

Vlt. ists noch zu früh aber ich steig da noch nicht ganz durch, wie das gehen soll.
Man meldet sich ja beim php server an über ein login und nur wenn der login erfolgreich war darf man sich am node server anmelden, wie soll das ohne nur eine einzige kommunikation funktionieren?

danke erstmal für die ausführliche antwort :)
 
Zuletzt bearbeitet:
Ok, ich probiers nochmal verständlicher. Dabei werde ich erst darauf eingehen, wie es implementiert werden muss und daraufhin warum.

1. Eine PHP-Anwendung wird aufgerufen und erzeugt die HTML Ausgaben. Dabei erzeugt die Anwendung bei jedem Aufruf neu eine dynamische URL für den SSE-Server. Die Url wird folgendermaßen gebaut:
Code:
[B]UserId[/B] = UserId des Nutzers
[B]Nonce[/B] = zufällig berechnete Zeichenkette aus X Zeichen
[B]Zeitstempel[/B] = aktuelle Zeit (Unix-Timestamp)
[B]CookieHash[/B] = hmac_sha512(secret, Cookie[SessionId])
[B]SignedToken[/B] = hmac_sha512(secret, [B]UserId[/B] + [B]Nonce[/B] + [B]Zeitstempel[/B] + [B]CookieHash[/B])

URL = http://sse-server/?UserId=[B]Userid[/B]&Nonce=[B]Nonce[/B]&Zeitstempel=[B]Zeitstempel[/B]&cookie=[B]CookieHash[/B]&Token=[B]SignedToken[/B]

2. Der Java-Script-Teil wird dann aus dem Dokument irgendwo diese URL auslesen und damit eine SSE-Verbindung aufbauen

3. Der NodeJS-Server bekommt eine Abfrage mit der URL. Er wird die GET-Parameter der URL ebenso hashen und mit dem Token aus dem GET vergleichen. Ist der Token identisch, entspricht der Wert von CookieHash ebenso dem hmac sha512 des echten Cookies und ist der Zeitstempel nicht länger als 60s her ist der Request in Ordnung. Nun wird noch geprüft ob der Token auf einer Blacklist im Nodejs-Server steht. Ist dem nicht so, ist die Anfrage korrekt und die SSE-Logik kann einsetzen. Zusätzlich wird der Token für die nächsten 60 Sekunden in die Blackliste eingetragen.


Das ist die Vorgehensweise. Der Node.js Server kann also ohne Kommunikation mit dem PHP-Teil bestimmen, ob jemand berechtigt ist für die UserId Events zu empfangen. Nun zur Erklärung warum manche Dinge gemacht wurden:

Hmac_Sha512: Hmac ist ein Algorithmus um Daten zu signieren, man kann also sicherstellen, dass die Daten nicht modifiziert wurden, wenn der Hmac-Hash übereinstimmt. Und genau das machen wir: Wir berechnen den Hash durch PHP und er landet dann bei Node.js, würde der Client die Parameter modfizieren, würde der Hash nicht mehr übereinstimmen und Node.js würde es als einen Angriff erkennen. Natürlich ist das geheime Password "secret" nur dem PHP- und Node.js-Server bekannt, niemandem anderen. Das ganze kann auch OpenSource ist, ist alles kein Problem, aber jede Installation sollte einen anderen Secret haben ;)

Nonce: Die Idee ist einfach nur, dass jeder Token "einmalig" ist. Würde ein Nutzer in der gleichen Millisekunde 2mal die Seite aufrufen, würde er die gleiche URL bekommen, kann sich aber nur einmal mit dem Node.js-Server verbinden - wegen der Blackliste. Die Nonce sorgt dafür, dass dieses Problem nicht auftritt.

CookieHash: Das Cookie sollte im Klartext nicht im Get-Teil übertragen werden, da es dann in Webserver-Logs auftaucht. Deswegen wird es für die Übergabe als Get-Parameter nochmal mit hmac_sha512 gehasht. Der Cookie wird übertragen, damit man sichergehen kann, dass der gleiche Nutzer das PHP-Script aufgerufen hat und auch den Node.js-Server aufruft. Denn der PHP-Teil kennt ja den Wert des Cookies, der zu einem Nutzer gehört. Aber der Node.js weiß die Zuordnung Cookie->UserId nicht, deswegen wird sie als extra Parameter in der URL mitgeschickt. Du kannst den Teil hier noch erweitern um die IP, oder die Browserkennung. Aber der Cookie-Wert ist mindestens nötig.

Zeitstempel: Der wird für die Blackliste im Nodejs-Server genutzt. Die Idee ist einfach, dass jede URL nur 60 Sekunden gültig sein soll. Wurde bis dahin die SSE-Verbindung nicht aufgebaut (sehr unrealistisch) dann wird sie nicht mehr aktzeptiert. Die Begründung liegt darin, dass kein böser Nutzer die Möglichkeit haben soll im Browsercache eines Nutzer die URL auszulesen und dann irgendwann nochmal zu verbinden, möglicherweise 1 Tag später.


Ist es jetzt verständlicher? Die Idee baut eben darauf auf, dass der PHP-Teil eben alle Infos mitsendet, die der Nodejs-Server braucht. Die gesendeten Daten werden durch den Hmac-Hash vor Modifikationen geschützt. Das man sich nicht als anderen Nutzer ausgeben kann ist durch den Wert des Cookies geregelt. Ändert sich der Cookie, ist der ganze signierte Token anders und man wird nicht der gleiche Nutzer sein. Genauso wenn man die UserId manuell ändert, die None oder den Zeitstempel. Modifiziert ein Nutzer irgendetwas, weiß der Node.js-Server dies, und lehnt die Verbindung ab. Wurde keine Modifikation festgestellt kann der Node.js-Server aber den Nutzer identifizieren, da er weiß welchen Wert das SessionId-Cookie haben muss. Trifft dies zu und ist der gesamte Token identisch, weiß der Node.js-Server, dass die UserId aus dem Get-Request die wirkliche User-Id des Clients ist.

Wieviele Browserfenster ein Nutzer nutzen möchte, ob alle die gleiche IP haben usw. ist egal. Eine signierte erzeugte URL von PHP kann genau einmal innerhalb von 60 Sekunden genutzt werden, um zum Nodejs-Server zu verbinden. Danach muss eine neue URL erzeugt werden. Ob das nachher ein Page-Reload oder ein Ajax-PHP-Request ist, ist irrelevant.
 
Zuletzt bearbeitet:
woha, mit einer so ausfuehrlichen antwort haette ich nicht gerechnet. habs - da in der bahn - nur ueberflogen, aber wow, danke danke danke!
mir ist das ganze nun tatsaechlich ein wenig verstaendkicher geworden, setze mich morgen mal an die umsetzung. hast mir wirklich ubglaublich geholfen!
super erklaerung!
um es kurz zu machen danke, haette ich nicht mit gerechnet.
evtl. meld ich mich morgen nochmal.

p.s.
respekt das dir das mal so nebenbei einfaellt
 
Keine Angst, ist mir auch nicht just diese Sekunde eingefallen ;) Die Lösung hat sich über mehrere Monate entwickelt, und nutze ich aktuell um mir die Kommunikation von Servern, die über Deutschland verteilt sind, zu sparen. So wie bei dir die Kommunikation mit PHP- und Node.js-Server auf der gleichen physischen Maschine.

Wenn noch irgendeine Unklarheit warum irgendetwas gemacht wird einfach fragen ;)
 
Zurück
Oben