Index: slim/server/HTML/EN/html/docs/http.html =================================================================== RCS file: /cvsroot/slim/server/HTML/EN/html/docs/http.html,v retrieving revision 1.8 diff -u -r1.8 http.html --- slim/server/HTML/EN/html/docs/http.html 3 Aug 2004 17:28:54 -0000 1.8 +++ slim/server/HTML/EN/html/docs/http.html 7 Jan 2005 14:40:50 -0000 @@ -335,6 +335,45 @@ to have infrared debugging output. +

CSRF Security Measures

+

To protect against "Cross Site Request Forgery" (CSRF) security threats, SlimServer applies special scrutiny to HTTP requests for functions that can make changes to your system or manipulate playlists or players. +This scrutiny is important to understand if you want to "bookmark" such a URL, or control SlimServer via the HTTP interface. +SlimServer requires one of two things to be true in order to allow such control-oriented URLs to be accepted:
+

+
Referer request header matches SlimServer URL +
SlimServer will accept an HTTP URL/command if the request includes a "Referer" request header that matches the "Host" request header. +If you're using a web browser and clicking on links in the web interface (for instance, the Previous/Skip/Play/Pause links on the right-hand player pane), your browser will send a Referer header that tells SlimServer it's following a link you clicked on from a SlimServer page, and the request will be accepted. +If you are using custom software to make the HTTP requests, simply make sure the HTTP request includes a Referer header with the same value as the URL you are requesting, and a Host header that includes the :port information (as described in the HTTP standard) and all will be fine. +
+ +
"cauth" URL parameter presented and valid +
If you're using a Bookmark/Favorite, no Referer rewuest header is sent. +In such a case, SlimServer cannot tell if your request is legitimate, or your browser following an <IMG> tag in a hostile web page or HTML email. +In such cases, you will see a "403 Forbidden" error page. +On this page you'll see a URL similar to what you requested, but with a ";cauth=" parameter added to the end, e.g. when denying a bookmarked request for http://10.0.1.201:9000/status.html?p0=rescan, SlimServer will offer you a clickable link with a URL like

+http://10.0.1.201:9000/status.html?p0=rescan;cauth=aa2d378f7e9f18611e951e7c6b30eea8

+By clicking on this link, you can execute your HTTP command. +If you want to bookmark a URL that gives such an error, bookmark the "cauth" URL displayed on the error page. + +"cauth" URIs are unique for each SlimServer installation. They are based on a special "securitySecret" that is randomly assigned the first time you start the SlimServer software. +This allows the same URL like +http://10.0.1.201:9000/status.html?p0=rescan;cauth=aa2d378f7e9f18611e951e7c6b30eea8 +to always work on your SlimServer installation (so you can bookmark it or use it in your home automation system), but not work on another SlimServer setup. +This unpredicatbility makes it virtually impossible for a hostile web site to trick your SlimServer into doing something you don't want. +

+By default, the same "cauth" value is accepted for any URI. +So once you note the "cauth" value for your system, you can use that same value with any URI/command documented here. +This is the "MEDIUM" level for the "csrfProtectionLevel" server preference. +If you have trouble using the web interface -- for instance, some non-standard browsers do not send "Referer" headers with their requests -- you may change your csrfProtectionLevel to "NONE". This is generally not recommended, but is available to you. +

+If you want greater protection against CSRF attacks, you may set your csrfProtectionLevel server preference to "HIGH" so that each different command/URI will insist on a unique (but reusable) "cauth=" value. +For instance, the "cauth" parameter for http://10.0.1.201:9000/status.html?p0=playlist&p1=play&p2=soothing would be completely different for the parameter for http://10.0.1.201:9000/status.html?p0=playlist&p1=play&p2=loud-alarm +This also makes it more difficult for an attacker to trick your SlimServer into doing something you don't want. +
+ +

Multiple Players

Finally, if more than one player is connected to the system, you can specify that player with a unique player identifier for the device being controlled. This unique identifyer is generated when the player connects. This identifier may be in the form of an IP address or MAC address, depending on the kind of client that's connecting.

@@ -570,4 +609,4 @@ /Volumes/10.0.1.201/A Tribe Called Quest/The Low End Theory/13. What?.mp3 /Volumes/10.0.1.201/A Tribe Called Quest/The Low End Theory/14. Scenario.mp3 -[% PROCESS helpfooter.html %] \ No newline at end of file +[% PROCESS helpfooter.html %] Index: slim/server/HTML/EN/html/errors/403.html =================================================================== RCS file: /cvsroot/slim/server/HTML/EN/html/errors/403.html,v retrieving revision 1.2 diff -u -r1.2 403.html --- slim/server/HTML/EN/html/errors/403.html 1 Apr 2004 02:31:07 -0000 1.2 +++ slim/server/HTML/EN/html/errors/403.html 7 Jan 2005 14:40:50 -0000 @@ -1,2 +1,4 @@ 403 Forbidden -403 Forbidden: [% path %] +403 Forbidden: [% path %] +[% validURL %] + Index: slim/server/Slim/Utils/Prefs.pm =================================================================== RCS file: /cvsroot/slim/server/Slim/Utils/Prefs.pm,v retrieving revision 1.96 diff -u -r1.96 Prefs.pm --- slim/server/Slim/Utils/Prefs.pm 17 Dec 2004 10:09:37 -0000 1.96 +++ slim/server/Slim/Utils/Prefs.pm 7 Jan 2005 14:40:50 -0000 @@ -11,6 +11,7 @@ use File::Spec::Functions qw(:ALL); use File::Path; use FindBin qw($Bin); +use Digest::MD5; use Slim::Utils::Misc; use Slim::Hardware::IR; @@ -21,9 +22,31 @@ my $prefsFile; my $canWrite; +sub makeSecuritySecret { + # each SlimServer installation should have a unique, + # strongly random value for securitySecret. This routine + # will be called by checkServerPrefs() the first time + # SlimServer is started to "seed" the prefs file with a + # value for this installation + # + # do we already have a value? + my $currentVal = get('securitySecret'); + if (defined($currentVal) && ($currentVal =~ m|^[0-9a-f]{32}$|)) { + $::d_prefs && msg("server already has a securitySecret\n"); + return $currentVal; + } + # make a new value, based on a random number + my $hash = new Digest::MD5; + $hash->add(rand()); + # explicitly "set" this so it persists through shutdown/startupa + my $secret = $hash->hexdigest(); + $::d_prefs && msg("creating a securitySecret for this installation\n"); + set('securitySecret',$secret); + return $secret; +} + sub defaultAudioDir { my $path; - if (Slim::Utils::OSDetect::OS() eq 'mac') { $path = ($ENV{'HOME'} . '/Music'); @@ -98,6 +121,8 @@ ,"music" => defaultAudioDir() ,"playlistdir" => defaultPlaylistDir() ,"cachedir" => defaultCacheDir() + ,"securitySecret" => makeSecuritySecret() + ,"csrfProtectionLevel" => "MEDIUM" ,"skin" => "Default" ,"language" => "EN" ,"refreshRate" => 30 Index: slim/server/Slim/Web/HTTP.pm =================================================================== RCS file: /cvsroot/slim/server/Slim/Web/HTTP.pm,v retrieving revision 1.133 diff -u -r1.133 HTTP.pm --- slim/server/Slim/Web/HTTP.pm 20 Dec 2004 05:13:16 -0000 1.133 +++ slim/server/Slim/Web/HTTP.pm 7 Jan 2005 14:40:52 -0000 @@ -9,6 +9,7 @@ use strict; +use Digest::MD5; use Data::Dumper; use FileHandle; use File::Spec::Functions qw(:ALL); @@ -120,6 +121,17 @@ ); } +my %dangerousCommands = ( + # name of command => regexp for URI patterns that make it dangerous + # e.g. + # \&Slim::Web::Pages::status => '\bp0=rescan\b' + # means inisist on CSRF protection for the status command *only* + # if the URL includes p0=rescan + \&Slim::Web::Setup::setup_HTTP => '.', + \&Slim::Web::EditPlaylist::editplaylist => '.', + \&Slim::Web::Pages::status => '(p0=debug|p0=pause|p0=stop|p0=play|p0=sleep|p0=playlist|p0=mixer|p0=display|p0=button|p0=rescan|(p0=(|player)pref\b.*p2=[^\?]|p2=[^\?].*p0=(|player)pref))', +); + # initialize the http server sub init { idle(); @@ -284,6 +296,20 @@ join(' ', ($request->method(), $request->protocol(), $request->uri()), "\n") ); + # remove our special X-Slim-CSRF header if present + $request->remove_header("X-Slim-CSRF"); + + # store CSRF auth code in fake request header if present + if (defined($request->uri()) && ($request->uri() =~ m|^(.*)\;cauth\=([0-9a-f]{32})$|) ) { + my $plainURI = $1; + my $csrfAuth = $2; + $::d_http && msg("Found CSRF auth token \"$csrfAuth\" in URI \"".$request->uri()."\", so resetting request URI to \"$plainURI\"\n"); + # change the URI so later code doesn't "see" the cauth part + $request->uri($plainURI); + # store the cauth code in the request object (headers are handy!) + $request->push_header("X-Slim-CSRF",$csrfAuth); + } + # this bundles up all our response headers and content my $response = HTTP::Response->new(); @@ -422,25 +448,17 @@ $params->{"path"} = unescape($path); $params->{"host"} = $request->header('Host'); } - # setup URLs should not work from referrers that are not from the web interface. - if ($params->{"path"} && $pageFunctions{$params->{"path"}} && $pageFunctions{$params->{"path"}} eq \&Slim::Web::Setup::setup_HTTP) { - if ($request->header('Referer')) { - my ($host, $port, $path, $user, $password) = Slim::Utils::Misc::crackURL($request->header('Referer')); - if ("$host:$port" ne $request->header('Host')) { - # throw 403, we don't allow setup from non-server pages. - $params->{'suggestion'} = "Invalid referrer."; - $::d_http && msg("Invalid referer: [" . join(' ', ($request->method(), $request->uri())) . "]\n"); - - $response->code(RC_FORBIDDEN); - $response->content_type('text/html'); - $response->header('Connection' => 'close'); - $response->content(${filltemplatefile('html/errors/403.html', $params)}); - - $httpClient->send_response($response); - closeHTTPSocket($httpClient); - return; - } - } + + # apply CSRF protection logic to "dangerous" commands + foreach my $d ( keys %dangerousCommands ) { + my $dregexp = $dangerousCommands{$d}; + if ($params->{"path"} && $pageFunctions{$params->{"path"}} && $pageFunctions{$params->{"path"}} eq $d && $request->uri() =~ m|$dregexp| ) { + if ( ! isRequestCSRFSafe($request,$response) ) { + $::d_http && msg("client requested dangerous function/arguments and failed CSRF Referer/token test, sending 403 denial\n"); + throwCSRFError($httpClient,$request,$response,$params); + return; + } + } } # HTTP/1.1 Persistent connections or HTTP 1.0 Keep-Alives @@ -1714,6 +1732,132 @@ push @templateDirs, $dir; } +sub isCsrfAuthCodeValid($) { + my $req = shift; + my $csrfProtectionLevel = Slim::Utils::Prefs::get("csrfProtectionLevel"); + if (! defined($csrfProtectionLevel) ) { + # Prefs.pm should have set this! + $::d_http && msg("Server unable to determine CRSF protection level due to missing server pref\n"); + return 0; + } + if ( $csrfProtectionLevel eq 'NONE' ) { + # no protection, so we don't care + return 1; + } + my $uri = $req->uri(); + my $code = $req->header("X-Slim-CSRF"); + if ( (!defined($uri)) || (!defined($code)) ) { return 0; } + my $secret = Slim::Utils::Prefs::get("securitySecret"); + if ( (!defined($secret)) || ($secret !~ m|^[0-9a-f]{32}$|) ) { + # invalid secret! + $::d_http && msg("Server unable to verify CRSF auth code due to missing or invalid securitySecret server pref\n"); + return 0; + } + my $expectedCode = $secret; + # calculate what the auth code should look like + my $highHash = new Digest::MD5; + my $mediumHash = new Digest::MD5; + # only the "HIGH" cauth code depends on the URI + $highHash->add($uri); + # both "HIGH" and "MEDIUM" depend on the securitySecret + $highHash->add($secret); + $mediumHash->add($secret); + # a "HIGH" hash is always accepted + if ( $code eq $highHash->hexdigest() ) { return 1; } + if ( $csrfProtectionLevel eq 'MEDIUM' ) { + # at "MEDIUM" level, we'll take the $mediumHash, too + if ( $code eq $mediumHash->hexdigest() ) { return 1; } + } + # the code is no good (invalid or MEDIUM hash presented when using HIGH protection)! + return 0; +} + +sub isRequestCSRFSafe($$) { + my ($request,$response,$params) = @_; + my $rc = 0; + # referer test from SlimServer 5.4.0 code + if ($request->header('Referer') && defined($request->header('Referer')) && defined($request->header('Host')) ) { + my ($host, $port, $path, $user, $password) = Slim::Utils::Misc::crackURL($request->header('Referer')); + # if the Host request header lists no port, crackURL() reports it as port 80, so we should + # pretend the Host header specified port 80 if it did not + my $hostHeader = $request->header('Host'); + if ($hostHeader !~ m/:\d{1,}$/ ) { $hostHeader .= ":80"; } + if ("$host:$port" ne $hostHeader) { + $::d_http && msg("Invalid referer: [" . join(' ', ($request->method(), $request->uri())) . "]\n"); + } else { + # looks good + $rc = 1; + } + } + if ( ! $rc ) { + # need to also check if there's a valid "cauth" token + if ( ! isCsrfAuthCodeValid($request) ) { + $params->{'suggestion'} = "Invalid referrer and no valid cauth code."; + $::d_http && msg("No valid CSRF auth code: [" . join(' ', ($request->method(), $request->uri(), $request-header('X-Slim-CSRF'))) . "]\n"); + } else { + # looks good + $rc = 1; + } + } + return $rc; +} + +sub makeAuthorizedURI($) { + my $uri = shift; + my $secret = Slim::Utils::Prefs::get("securitySecret"); + if ( (!defined($secret)) || ($secret !~ m|^[0-9a-f]{32}$|) ) { + # invalid secret! + $::d_http && msg("Server unable to compute CRSF auth code URL due to missing or invalid securitySecret server pref\n"); + return undef; + } + my $csrfProtectionLevel = Slim::Utils::Prefs::get("csrfProtectionLevel"); + if (! defined($csrfProtectionLevel) ) { + # Prefs.pm should have set this! + $::d_http && msg("Server unable to determine CRSF protection level due to missing server pref\n"); + return 0; + } + my $hash = new Digest::MD5; + if ( $csrfProtectionLevel eq 'HIGH' ) { + # different code for each different URI + $hash->add($uri); + } + $hash->add($secret); + return $uri . ';cauth=' . $hash->hexdigest(); +} + +sub throwCSRFError($$$$) { + my ($httpClient,$request,$response,$params) = @_; + # throw 403, we don't this from non-server pages + # unless valid "cauth" token is present + $params->{'suggestion'} = "Invalid Referer and no valid CSRF auth code."; + my $protoHostPort = 'http://' . $request->header('Host'); + my $authURI = makeAuthorizedURI($request->uri()); + my $authURL = $protoHostPort . $authURI; + # add a long SGML comment so Internet Explorer displays the page + my $msg = "\n

"; + # BUG: stringify the following, as this message needs to be translatable! + $msg .= "In order to request this URL from a Bookmark/Favorite, or some means other than following a link from the SlimServer web interface, you will need to use a URL with a \"cauth\" security parameter. If you received this error when following a link from the SlimServer web interface, you will want to make sure your web browser software (including proxy servers and spyware/privacy software) is allowing \"Referer\" headers to be sent. Below is the appropriate URL for the URL you attempted."; + $msg .= "
\n
\n${authURL}

"; + my $csrfProtectionLevel = Slim::Utils::Prefs::get("csrfProtectionLevel"); + if ( defined($csrfProtectionLevel) && $csrfProtectionLevel eq 'MEDIUM' ) { + # BUG: stringify the following, as this message needs to be translatable! + $msg .= "

Because your CSRF protection level is set at 'MEDIUM', you can use the same ";cauth=" value for any URL; this means you should be more careful who you share your URLs with.

"; + } + $params->{'validURL'} = $msg; + # add the appropriate URL in a response header to make automated + # re-requests easy? (WARNING: this creates a potential Cross Site + # Tracing sort of vulnerability! + # (see http://computercops.biz/article2165.html for info on XST) + # If you enable this, also uncomment the text regarding this on the http.html docs + #$response->header('X-Slim-Auth-URI' => $authURI); + $response->code(RC_FORBIDDEN); + $response->content_type('text/html'); + $response->header('Connection' => 'close'); + $response->content(${filltemplatefile('html/errors/403.html', $params)}); + $httpClient->send_response($response); + closeHTTPSocket($httpClient); +} + 1; __END__