csrfToken-HowTo

Aus FHEMWiki
Wechseln zu: Navigation, Suche

FHEM hat mit der Version 5.8 eine Sicherheitsmaßnahme scharfgeschaltet, den csrfToken. Dieser Token wird bei jedem Neustart von FHEM neu gebildet.

Dieses Feature erhöht die Sicherheit, führt aber dazu, dass man nicht mehr mit einem einfachen http Link auf das FHEMWEB zugreifen kann.

Einzeiler

Was früher so ging:

http://localhost:8083/fhem?cmd=set%20Office%20on


muss jetzt etwas ergänzt werden. Mit dem Einzeiler

curl -s -D - 'http://localhost:8083/fhem?XHR=1' | awk '/X-FHEM-csrfToken/{print $2}'

kann man den aktuellle csrfToken aus dem Header extrahieren und muss ihn nur noch an den Aufruf anhängen.

Erste (Vorzugs) Variante:

curl --data "fwcsrf=$(curl -s -D - 'http://localhost:8083/fhem?XHR=1' | awk '/X-FHEM-csrfToken/{print $2}')" http://localhost:8083/fhem?cmd=set%20Office%20on

Oder mit mehr Komfort: Nur Einmal die Angabe des Hostnamen und den FHEM Befehl normal mit Leerzeichen angeben:

h='Name oder IP:PortNr'; c='FHEM Befehl'; curl --data "fwcsrf=$(curl -s -D - http://$h/fhem?XHR=1 | awk '/X-FHEM-csrfToken/{print $2}')" http://$h/fhem?cmd=$(echo $c|sed 's/ /%20/g')

Zweite Variante, in dieser wird cr+lf am Ende des ermittelten csrfTokens abgeschnitten:

curl "http://fhem.example.org:8083/fhem?cmd=set%20Office%20on&XHR=1&fwcsrf="`curl -s -D - 'http://fhem.example.org:8083/fhem?XHR=1' | awk '/X-FHEM-csrfToken/{print $2}' | tr -d "\r\n"`

Innerhalb FHEM mit Perl

FHEM kennt natürlich seinen Token und man hat einfachen Zugriff mit der Variable $FW_CSRF. Diese Varibale enthält nicht den Token an sich, sondern den kompletten String zum Anhängen an eine URL in der Form: &fwcsrf=csrf_387849633005507

Beispiel (funktioniert auch in der FHEM Kommandozeile)

{my $button_test = "<a href='http://localhost:8083/fhem?cmd=set%20Office%20on$FW_CSRF'>test</a>"}

Raw Import in der Shell

Will man ganze Definitionsblöcke von der System Kommandozeile importieren, kann man dieses Script verwenden. Im übergebenen Dateinamen sind Zeilenweise die FHEM Befehle gespeichert. Bei Bedarf kann man auch einen anderen Hostnamen und ein anderes Port angeben. Der Token wird zu Beginn ermittelt. Die Leerzeichen werden für HTTP durch %20 ersetzt.

#!/bin/bash
# fhemraw Import analog Raw Definition
if [ $# -eq 0 ] ; then
     echo fhemraw bitte so verwenden
     echo fhemraw <dateiName> [<hostName>] [<portNummer>]
     exit 1
fi

if [ -z $2 ] ; then
     fhemhost=localhost
   else
     fhemhost=$2
fi
if [ -z $3 ] ; then
     fhemhost=$fhemhost:8083
   else
     fhemhost=$fhemhost:$3
fi
token=$(curl -s -D - "http://$fhemhost/fhem?XHR=1" | awk '/X-FHEM-csrfToken/{print $2}')

while read line
do
    curl --data "fwcsrf=$token" http://$fhemhost/fhem?cmd=$(echo $line|sed 's/ /%20/g')
done < $1

API Web

Falls man ohne den Token arbeiten will, könnte man ein eigenes API Web erstellen und den Zugriff darauf beschränken. In vorhandenen Scripten / Applikationen müsste dann lediglich der Port geändert werden.

 define WEBapi FHEMWEB 8088 global
 attr WEBapi csrfToken none
 attr WEBapi allowfrom 192.168.178.83|127.0.0.1

Im Forum ist beschrieben wie man die Gestaltung des regEx für die IP Adresse machen kann (Thema). Einen regEx Builder für IP-Ranges findet man unter [1]

csrfToken festlegen

Dies kann man tun, falls die dynamische Abfrage zur Laufzeit des Tokens nicht möglich ist.

 attr WEB.* csrfToken <beliebige Folge aus Zeichen und Zahlen>

Damit können feste URLs verwendet werden:

 http://localhost:8083/fhem?cmd=set%20Office%20on&fwcsrf=<fester token>

csrfToken abschalten

Dies sollte man als erste Hilfe tun, aber unbedingt darüber nachdenken wie man die Applikation umstellt.

 attr WEB.* csrfToken none

Featurelevel

Eine weitere temporäre Notfallmaßnahme wäre den Featurelevel nach dem Update einfach wieder zurückzudrehen

 attr global featurelevel 5.7

Python

Falls mal jemand aus python (hier für python 2.7) heraus fhem ansteuern möchte

import sys
import urllib2
import urllib
import ssl
import urlparse
BASEURL = 'https://user:password@server_ip:8083/fhem?'
url = BASEURL + 'cmd=set+licht+on'
def get_token(url):
    nurl = urlparse.urlsplit(url)
    username = nurl.username
    password = nurl.password
    url = url.replace(username + ':' + password + '@', '')
    url = url.replace(" ", "%20")
    ssl._create_default_https_context = ssl._create_unverified_context
    p = urllib2.HTTPPasswordMgrWithDefaultRealm()
    p.add_password(None, url, username, password)
    handler = urllib2.HTTPBasicAuthHandler(p)
    opener = urllib2.build_opener(handler)
    urllib2.install_opener(opener)
    try:
        uu = urllib2.urlopen(
            url=url,
            data=None,
            timeout=10
        )
        token = uu.read()
        token = token[token.find('csrf_'):]
        token = token[:token.find("\'")]
        return token
    except urllib2.URLError, urllib2.URLError.reason:
        print('URLError: %s' % urllib2.URLError.reason)
        return False

def fire_command(url):
    # type: (object) -> object
    if "@" in url:
        token = get_token(BASEURL)
        data = {'fwcsrf': token}
        data = urllib.urlencode(data)
        nurl = urlparse.urlsplit(url)
        username = nurl.username
        password = nurl.password
        url = url.replace(username + ':' + password + '@', '')
        url = url.replace(" ", "%20")
        ssl._create_default_https_context = ssl._create_unverified_context
        p = urllib2.HTTPPasswordMgrWithDefaultRealm()
        p.add_password(None, url, username, password)
        handler = urllib2.HTTPBasicAuthHandler(p)
        opener = urllib2.build_opener(handler)
        urllib2.install_opener(opener)
        try:
            urllib2.urlopen(
                url=url,
                data=data,
                timeout=10
            )
        except urllib2.URLError, urllib2.URLError.reason:
            print('URLError: %s' % urllib2.URLError.reason)
            return False

Links