CsrfToken-HowTo: Unterschied zwischen den Versionen

Aus FHEMWiki
(Anleitung zur Problembehebung im Zusammenhang mit dem csrfToken)
 
K (Webhook sinnvoll eingebaut)
 
(31 dazwischenliegende Versionen von 9 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
FHEM hat mit der Version 5.8 eine Sicherheitsmaßnahme scharfgeschaltet, den csrfToken.  
{{SEITENTITEL:csrfToken-HowTo}}
==Vorbemerkung==
FHEM hat mit der Version 5.8 eine Sicherheitsmaßnahme scharfgeschaltet, den csrfToken.
Dieser Token wird bei jedem Neustart von FHEM neu gebildet.
Dieser Token wird bei jedem Neustart von FHEM neu gebildet.


Diese Feature erhöht die Sicherheit, führt aber dazu, dass man nicht mehr mit einem einfachen http Link auf das FHEMWEB zugreifen kann.
Dieses Feature erhöht die Sicherheit (siehe Links ganz unten), führt aber dazu, dass man nicht mehr (wie in älteren Anleitungen beschrieben), mit einem statischen http Link Steuerbefehle (auch Webhooks) an das [[FHEMWEB]] senden kann.


==Einzeiler==
'''Beispiel'''


Was früher so ging:
Schalte den "Schalter" Office auf on. Das ging früher ohne CsrfToken einfach durch diesen HTML String - egal ob im Browser, in Windows, auf dem Mac, auf der Kommandozeile mit wget usw.
  http://localhost:8083/fhem?cmd=set%20Office%20on
:<syntaxhighlight lang="html">http://hostname:8083/fhem?cmd=set%20Office%20on</syntaxhighlight>
muss jetzt etwas ergänzt werden. Mit dem Einzeiler
Mit CsrfToken müsste diese Zeile so aussehen (Beispiel, der Token ändert sich bei jedem Start von FHEM):
  curl -s -D - 'http://localhost:8083/fhem&XHR=1' | awk '/X-FHEM-csrfToken/{print $2}
:<syntaxhighlight lang="html">http://hostname:8083/fhem?cmd=set%20Office%20on&fwcsrf=csrf_196525024154371</syntaxhighlight>
kann man den aktuellle csrfToken aus dem Header extrahieren und muss ihn nur noch an den Aufruf anhängen:
Ist der CsrfToken falsch (weil durch Neustart geändert) erfolgt keine Reaktion Schalters und es gibt eine (ähnliche) Fehlermeldung im FHEMLog:
  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
   2019.10.24 12:08:13 3: FHEMWEB WEB CSRF error: csrf_104345683644172 ne csrf_148832911104462 for client WEB_192.168.178.20_54197 ...
Man kann auch in Scripten den token zunächst abspeichern:
''Diese Fehlermeldung kann selten auch so beim Neustart auftauchen, wenn ein Browser Fenster mit altem Token nach einem "shutdown restart" einfach nur einen Refresh macht.''
   token=$(curl -s -D - 'http://localhost:8083/fhem&XHR=1' | awk '/X-FHEM-csrfToken/{print $2}')
  curl --data "fwcsrf=$token" http://localhost:8083/fhem?cmd=set%20Office%20on


==API Web==
Soll ein http: Link zum Steuern von FHEM verwendet werden, gibt es folgende Lösungsansätze:
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.  
 
==Codebeispiele==
===Einzeiler Shell===
 
Entweder mit curl oder wget
 
:<syntaxhighlight lang="bash">curl -s -D - 'http://localhost:8083/fhem?XHR=1' | awk '/X-FHEM-csrfToken/{print $2}'
wget -qO - --server-response 'http://localhost:8083/fhem?XHR=1' 2>&1 | awk '/X-FHEM-csrfToken/{print $2}'</syntaxhighlight>
 
kann man den aktuellen csrfToken aus dem Header extrahieren und muss ihn nur noch an den Aufruf anhängen.
 
Erste Variante mit curl:
 
:<syntaxhighlight lang="bash">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</syntaxhighlight>
 
Oder mit mehr Komfort (eigentlich ein Mehrzeiler): Nur Einmal die Angabe des Hostnamen und den FHEM Befehl normal mit Leerzeichen angeben:
:<syntaxhighlight lang="bash">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')</syntaxhighlight>
 
Ein übersichtlicher Mehrzeiler mit wget. Als Beispiel wird vom localhost:8083 ein "list global" ohne HTML ausgegeben:
 
:<syntaxhighlight lang="bash">hosturl="http://localhost:8083"
cmd=list%20global
token=$(wget -qO - --server-response "$hosturl/fhem?XHR=1" 2>&1 | awk '/X-FHEM-csrfToken/{print $2}')
wget -q -O - "$hosturl/fhem?cmd=$cmd&fwcsrf=$token&XHR=1"</syntaxhighlight>
 
Man kann auch noch den token vom cr+lf am Ende befreien:
 
:<syntaxhighlight lang="bash">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"`</syntaxhighlight>
 
===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)
:<syntaxhighlight lang="perl">{my $button_test = "<a href='http://localhost:8083/fhem?cmd=set%20Office%20on$FW_CSRF'>test</a>"}
</syntaxhighlight>
 
=== Perl Sub ===
Innerhalb von Perl geht die Abfrage eines anderen FHEM Servers natürlich auch:<syntaxhighlight lang="perl">
sub fhemcl
{
  use URI::Escape;
  use LWP::UserAgent;
  # if url is empty, localhost and default port is used
    my $fhemcmd = shift // return "Usage: fhemcl('fhem cmd'[,'urlToHost']) - like fhemcl('set Test2 toggle','http://host:8083')\n";
    my $hosturl = shift || 'http://localhost:8083';
    my $token;
  # get token
    my $ua = new LWP::UserAgent;
    my $url = "$hosturl/fhem?XHR=1/";
    my $resp = $ua->get($url);
        $token = $resp->header('X-FHEM-CsrfToken');
  # url encode the cmd
    $fhemcmd = uri_escape($fhemcmd);
    $url = "$hosturl/fhem?cmd=$fhemcmd&fwcsrf=$token&XHR=1";
    $resp = $ua->get($url)->content;
  # cut the last character: the additional newline
    return substr $resp,0,-1
}
</syntaxhighlight>
 
===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.
:<syntaxhighlight lang="bash">
#!/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
</syntaxhighlight>
 
===Python===
Falls mal jemand aus python (hier für python 2.7) heraus fhem ansteuern möchte
 
<syntaxhighlight lang="Python">
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
</syntaxhighlight>
 
==Alternative Konfiguration==
===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
   define WEBapi FHEMWEB 8088 global
   attr WEBapi csrfToken none
   attr WEBapi csrfToken none
   attr WEBapi allowFrom 192.168.178.83|127.0.0.1
   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 ({{Link2Forum|Topic=23994}}).
Einen regEx Builder für IP-Ranges findet man unter
[http://www.analyticsmarket.com/freetools/ipregex]
 
===csrfToken festlegen===
Dies kann man tun, falls die dynamische Abfrage zur Laufzeit des Tokens nicht möglich ist.
:<code><nowiki>attr WEB.* csrfToken <beliebige Folge aus Zeichen und Zahlen></nowiki></code>


Hier ist ganz gut beschrieben wie man die Gestaltung des regEx für die IP Adresse machen kann -> https://forum.fhem.de/index.php?topic=23994.0
Damit können feste URLs verwendet werden:
:<code><nowiki>http://localhost:8083/fhem?cmd=set%20Office%20on&fwcsrf=<fester token></nowiki></code>


==csrfToken abschalten==
===csrfToken abschalten===
Dies sollte man als erste Hilfe tun, aber unbedingt darüber nachdenken wie man die Applikation umstellt.
Dies sollte man als erste Hilfe tun, aber unbedingt darüber nachdenken wie man die Applikation umstellt.
:<code>attr WEB.* csrfToken none</code>


  attr WEB.* csrfToken none
===Featurelevel===
Eine weitere temporäre Notfallmaßnahme wäre den Featurelevel nach dem Update einfach wieder zurückzudrehen
:<code>attr global featurelevel 5.7</code>


==Featurelevel==
==Links==
Eine weitere temporäre Notfallmaßnahme wäre den Featurelevel nach dem Update einfach wieder zurückzudrehen
* Automatisch das korrekte Token in Weblink einbinden: {{Link2Forum|Topic=67543|Message=590584}}
* [https://de.wikipedia.org/wiki/Cross-Site-Request-Forgery Wikipedia - Cross Site Request Forgery (csrf)]
* [http://heinz-otto.blogspot.de/2017/03/csrf-token-und-fhem.html Blog-Beitrag zu csrf-Token und FHEM]


  attr global featurelevel 5.7
[[Kategorie:HOWTOS]]
[[Kategorie:Glossary]]

Aktuelle Version vom 27. März 2021, 11:26 Uhr

Vorbemerkung

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 (siehe Links ganz unten), führt aber dazu, dass man nicht mehr (wie in älteren Anleitungen beschrieben), mit einem statischen http Link Steuerbefehle (auch Webhooks) an das FHEMWEB senden kann.

Beispiel

Schalte den "Schalter" Office auf on. Das ging früher ohne CsrfToken einfach durch diesen HTML String - egal ob im Browser, in Windows, auf dem Mac, auf der Kommandozeile mit wget usw.

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

Mit CsrfToken müsste diese Zeile so aussehen (Beispiel, der Token ändert sich bei jedem Start von FHEM):

http://hostname:8083/fhem?cmd=set%20Office%20on&fwcsrf=csrf_196525024154371

Ist der CsrfToken falsch (weil durch Neustart geändert) erfolgt keine Reaktion Schalters und es gibt eine (ähnliche) Fehlermeldung im FHEMLog:

 2019.10.24 12:08:13 3: FHEMWEB WEB CSRF error: csrf_104345683644172 ne csrf_148832911104462 for client WEB_192.168.178.20_54197 ...

Diese Fehlermeldung kann selten auch so beim Neustart auftauchen, wenn ein Browser Fenster mit altem Token nach einem "shutdown restart" einfach nur einen Refresh macht.

Soll ein http: Link zum Steuern von FHEM verwendet werden, gibt es folgende Lösungsansätze:

Codebeispiele

Einzeiler Shell

Entweder mit curl oder wget

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

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

Erste Variante mit curl:

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 (eigentlich ein Mehrzeiler): 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')

Ein übersichtlicher Mehrzeiler mit wget. Als Beispiel wird vom localhost:8083 ein "list global" ohne HTML ausgegeben:

hosturl="http://localhost:8083"
cmd=list%20global
token=$(wget -qO - --server-response "$hosturl/fhem?XHR=1" 2>&1 | awk '/X-FHEM-csrfToken/{print $2}')
wget -q -O - "$hosturl/fhem?cmd=$cmd&fwcsrf=$token&XHR=1"

Man kann auch noch den token vom cr+lf am Ende befreien:

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>"}

Perl Sub

Innerhalb von Perl geht die Abfrage eines anderen FHEM Servers natürlich auch:

sub fhemcl
{
   use URI::Escape;
   use LWP::UserAgent;
   # if url is empty, localhost and default port is used
     my $fhemcmd = shift // return "Usage: fhemcl('fhem cmd'[,'urlToHost']) - like fhemcl('set Test2 toggle','http://host:8083')\n";
     my $hosturl = shift || 'http://localhost:8083';
     my $token;
   # get token 
     my $ua = new LWP::UserAgent;
     my $url = "$hosturl/fhem?XHR=1/";
     my $resp = $ua->get($url);
        $token = $resp->header('X-FHEM-CsrfToken');
   # url encode the cmd
     $fhemcmd = uri_escape($fhemcmd);
     $url = "$hosturl/fhem?cmd=$fhemcmd&fwcsrf=$token&XHR=1";
     $resp = $ua->get($url)->content;
   # cut the last character: the additional newline
     return substr $resp,0,-1
}

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

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

Alternative Konfiguration

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

Links