Closed alvvdc closed 3 years ago
Anything in the logs?
Anything in the logs?
Nothing in Nextcloud logs with debug 0 level. Nothing in PHP logs. In Lighttpd:
192.168.1.8 192.168.1.29 - [09/Sep/2019:15:08:08 +0100] "GET /nextcloud/index.php/logout?requesttoken=FfsiGZcBHPF%2BqSnsIhrlZNOXbAr%2F682WV718xe7WqfI%3D%3AfI1VL8ZUKLY4hlqKbG2OIpWlLlO6qJuiP9kT9qfn7MY%3D HTTP/1.1" 412 11746 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0"
Hi, I have the same problem running nextcloud on a rasberry PI3 using DietPi/Nextcloud/Lighttpd and reproduced as well running this system in a VM using virtual box. Setting the logging of nextcloud to debug I get:
[core] Debug: OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException: CSRF check failed at <
GET /nextcloud/index.php/logout?requesttoken=DTGY0BwWUvscxsw7frSaWhfTaRezU1t9SeeJGGXoF%2BQ%3D%3AfXqpk1NEZqNyrr9YDPL5KFWQEFLhZTIuI9%2FmQRCQZIE%3D from 192.168.16.37 by admin at 2019-09-23T14:33:41+00:00
Nextcloud Version: 16.0.4.1 Installierte Apps: 36 Apps mit verfügbaren Aktualisierungen: 1
PHP Version: 7.3.9 Arbeitspeicher-Grenzwert: 128 MB Maximale Ausführungszeit: 3600 Maximale Größe zum Hochladen: 8388608 TB
Datenbank Art: mysql Version: 10.3.17 Größe: 1,3 MB
The bug is easy to reproduce. Tell me, if more traces are needed. I can deliver them quickly. As it is now, the combination Nextcloud/Lighttpd can't be used productively. If you want to reproduce it by yourself do the following steps: 1) Install virtual box https://www.virtualbox.org/ 2) Download DietPi for Virtual Box and install it https://dietpi.com/ 3) start dietpi-software and install nextcloud 4) start browser on host system and login to nextcloud page of guest system 5) logout nextcloud in browser The fault shows up extremly often. Typically you need 5 faulty logout trys until you have a successful one.
1. Install virtual box https://www.virtualbox.org/ 2. Download DietPi for Virtual Box and install it https://dietpi.com/ 3. start dietpi-software and install nextcloud 4. start browser on host system and login to nextcloud page of guest system 5. logout nextcloud in browser The fault shows up extremly often. Typically you need 5 faulty logout trys until you have a successful one.
Logout works in Chromium and Firefox.
cc @MichaIng any idea?
OK. I can reproduce it somehow. Like you said login, upload some files, delete some files or do something else and try to logout afterwards.
public function isTokenValid(CsrfToken $token): bool {
if(!$this->sessionStorage->hasToken()) {
return false;
}
$isEqual = hash_equals($this->sessionStorage->getToken(), $token->getDecryptedValue());
if (!$isEqual) {
\OC::$server->getLogger()->warning('going to validate csrf token "' . $this->sessionStorage->getToken() . '" with "' . $token->getDecryptedValue() . '"');
}
return $isEqual;
}
Added some logging to isTokenValid
and ...
cc @rullzer @ChristophWurst :confused:
Could you add some logging to \OC\Security\CSRF\CsrfToken::getDecryptedValue so we see the values of the individual parts?
I was able to replicate as well on fresh NC17 installed on DietPi Buster VirtualBox image. @kesselb you tested on DietPi as well or another system? Just in case our default Lighttpd config on Debian Buster based images:
Mostly Debian package defaults, the custom /nextcloud part is at the bottom.
PHP is as well default php7.3-fpm
+ APCu
memory caching + Redis
-based file locking. I can provide more details if required, just not sure what might play a role here 😉
I have modified the source to get some additional logging: public function getDecryptedValue(): string { $token = explode(':', $this->value); if (\count($token) !== 2) { return ''; } $obfuscatedToken = $token[0]; $secret = $token[1]; \OC::$server->getLogger()->info('obfuscatedToken: '. $obfuscatedToken .', secret: '. $secret); $tmpReturn = base64_decode($obfuscatedToken) ^ base64_decode($secret); \OC::$server->getLogger()->info('getDecryptedValue: '. $tmpReturn ); return base64_decode($obfuscatedToken) ^ base64_decode($secret); } }
With this I did the ususal test: 1) login as admin 2) goto user / protocol and see the logging 3) logout
this time successfull
4) login as admin 5) goto user / protocol and see the logging 6) logout (unsuccessfull) 7) push browsers back button 8) logout (unsuccessfull) ... until successfull logout
Traces are attached. For details see the readme. logs+traces.zip
Problem is not yet solved, additional logs and stack trace provided above. Issue, although Lighttpd not officially supported, is security-relevant, e.g. on public clients:
I'm getting the same error with nginx, though also on https://mysite/index.php/apps/files/ajax/getstoragestats.php?dir=%2F.
I was able to reproduce this with nginx and front_controller_active true
:
/logout?requesttoken=abc
returns CSRF check failed
, but index.php/logout?requesttoken=abc
works.
I also can reproduce this issue on NC 17.0.1 using nginx. Happy to provide logs if needed. If I click logout twice it returns me to login screen, then upon logging with a new user I get the error: Access forbidden CSRF check failed
I was also getting the same error in NC 16.x - it is a smallish home server so it doesn't bother me that and I trawled through a bunch of forum posts and proposed fixes for similar issues to no avail.
This fixed this problem for me:
https://github.com/nextcloud/server/issues/1075#issuecomment-274376615
I also think this issue is a duplicate of this one: https://github.com/nextcloud/server/issues/1075
@pbalm It is not an HTTPS-only issue. I get the same error when logging in+out via HTTP, hence forcing SSL/HTTPS is no solution there. I am testing with NC18 Beta 4 now and settings the two settings to "false" instead.
NC18 Beta 4 same issue.
'overwriteprotocol' => 'http',
'forcessl' => false,
Same issue (note I am testing HTTP here, to verify it is no HTTPS-only issue).
For testing/replicating:
If the issue does not occur, try to restart webserver (and clear browser cache?) and redo the above. I also restarted Redis and MariaDB, but pretty sure that is not required.
Tested with:
root@VM-Buster:~# lighttpd -v
lighttpd/1.4.53 (ssl) - a light and fast webserver
root@VM-Buster:~# php -v
PHP 7.3.11-1~deb10u1 (cli) (built: Oct 26 2019 14:14:18) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.11, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.3.11-1~deb10u1, Copyright (c) 1999-2018, by Zend Technologies
root@VM-Buster:~# mariadb -v
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 57
Server version: 10.3.18-MariaDB-0+deb10u1 Debian 10
Just to wrap up some information, when reading through #1075:
So even that the above things seem to have some influence for people, none of it is a definite reason, nor can related changes solve it definitely. Not sure if its somehow possible, CSRF check fails, for whatever reason, to immediately mark the user as logged out for that particular session?
@MichaIng thanks for the detailed debugging info. Could you add the logs @kesselb added to his instance and additionally log the values (ref https://github.com/nextcloud/server/issues/17065#issuecomment-536923540) so we can get a bit more info about why this check is failing?
@ChristophWurst I'll do. Any preference whether to do this on stable v17.0.2 or current v18 RC1? Latter makes more sense?
Thanks! It shouldn't matter :)
Okay I updated an instance to 18 RC2 and disabled all apps but log viewer to have latest code and remove any possible apps influence. Then edited:
lib/private/Security/CSRF/CsrfTokenManager.php
public function isTokenValid(CsrfToken $token): bool {
if(!$this->sessionStorage->hasToken()) {
return false;
}
// DEBUG START
$isEqual = hash_equals($this->sessionStorage->getToken(), $token->getDecryptedValue());
if (!$isEqual) {
\OC::$server->getLogger()->warning('going to validate csrf token "' . $this->sessionStorage->getToken() . '" with "' . $token->getDecryptedValue() . '"');
}
// DEBUG END
return hash_equals(
$this->sessionStorage->getToken(),
$token->getDecryptedValue()
);
}
lib/private/Security/CSRF/CsrfToken.php
public function getDecryptedValue(): string {
$token = explode(':', $this->value);
if (\count($token) !== 2) {
return '';
}
$obfuscatedToken = $token[0];
$secret = $token[1];
// DEBUG START
\OC::$server->getLogger()->info('obfuscatedToken: '. $obfuscatedToken .', secret: '. $secret);
$tmpReturn = base64_decode($obfuscatedToken) ^ base64_decode($secret);
\OC::$server->getLogger()->info('getDecryptedValue: '. $tmpReturn );
// DEBUG END
return base64_decode($obfuscatedToken) ^ base64_decode($secret);
}
getDecryptedValue
, obviously the log viewer poll triggers the event.Zugriff verboten
CSRF check failed
[no app in context] Info: obfuscatedToken: nCV07rTLYHtzW0rcmcNVgIwHAIBMdWeTSrnx8cXTck8=, secret: 1Uojl+2AKgpYDC+p6bY678RoUus2EgT1DPPem7GjQRY=
GET /nextcloud/index.php/apps/logreader/poll?lastReqId=Me8CKtvlgIogiTMhvoy3
from 192.168.178.21 by admin at 2020-01-09T22:20:49+00:00
[no app in context] Info: getDecryptedValue: IoWyYKJq+WeupuooHoRkzgcfFJ/jtp3Y GET /nextcloud/index.php/apps/logreader/poll?lastReqId=Me8CKtvlgIogiTMhvoy3 from 192.168.178.21 by admin at 2020-01-09T22:20:49+00:00
[no app in context] Info: obfuscatedToken: nCV07rTLYHtzW0rcmcNVgIwHAIBMdWeTSrnx8cXTck8=, secret: 1Uojl 2AKgpYDC p6bY678RoUus2EgT1DPPem7GjQRY= GET /nextcloud/index.php/logout?requesttoken=nCV07rTLYHtzW0rcmcNVgIwHAIBMdWeTSrnx8cXTck8%3D%3A1Uojl%2B2AKgpYDC%2Bp6bY678RoUus2EgT1DPPem7GjQRY%3D from 192.168.178.21 by admin at 2020-01-09T22:20:49+00:00
[no app in context] Info: getDecryptedValue: IoWyÔÁâípQÔGúm©Æ )³ál:7\wPJëñ GET /nextcloud/index.php/logout?requesttoken=nCV07rTLYHtzW0rcmcNVgIwHAIBMdWeTSrnx8cXTck8%3D%3A1Uojl%2B2AKgpYDC%2Bp6bY678RoUus2EgT1DPPem7GjQRY%3D from 192.168.178.21 by admin at 2020-01-09T22:20:49+00:00
[no app in context] Warning: going to validate csrf token "IoWyYKJq+WeupuooHoRkzgcfFJ/jtp3Y" with "IoWyÔÁâípQÔGúm©Æ )³ál:7\wPJëñÂ" GET /nextcloud/index.php/logout?requesttoken=nCV07rTLYHtzW0rcmcNVgIwHAIBMdWeTSrnx8cXTck8%3D%3A1Uojl%2B2AKgpYDC%2Bp6bY678RoUus2EgT1DPPem7GjQRY%3D from 192.168.178.21 by admin at 2020-01-09T22:20:49+00:00
[no app in context] Info: obfuscatedToken: nCV07rTLYHtzW0rcmcNVgIwHAIBMdWeTSrnx8cXTck8=, secret: 1Uojl 2AKgpYDC p6bY678RoUus2EgT1DPPem7GjQRY= GET /nextcloud/index.php/logout?requesttoken=nCV07rTLYHtzW0rcmcNVgIwHAIBMdWeTSrnx8cXTck8%3D%3A1Uojl%2B2AKgpYDC%2Bp6bY678RoUus2EgT1DPPem7GjQRY%3D from 192.168.178.21 by admin at 2020-01-09T22:20:49+00:00
[no app in context] Info: getDecryptedValue: IoWyÔÁâípQÔGúm©Æ )³ál:7\wPJëñ GET /nextcloud/index.php/logout?requesttoken=nCV07rTLYHtzW0rcmcNVgIwHAIBMdWeTSrnx8cXTck8%3D%3A1Uojl%2B2AKgpYDC%2Bp6bY678RoUus2EgT1DPPem7GjQRY%3D from 192.168.178.21 by admin at 2020-01-09T22:20:49+00:00
[no app in context] Info: obfuscatedToken: nCV07rTLYHtzW0rcmcNVgIwHAIBMdWeTSrnx8cXTck8=, secret: 1Uojl+2AKgpYDC+p6bY678RoUus2EgT1DPPem7GjQRY= GET /nextcloud/index.php/apps/logreader/poll?lastReqId=IBpRq6Ll1fjC4vocsFCP from 192.168.178.21 by admin at 2020-01-09T22:20:49+00:00
[no app in context] Info: getDecryptedValue: IoWyYKJq+WeupuooHoRkzgcfFJ/jtp3Y GET /nextcloud/index.php/apps/logreader/poll?lastReqId=IBpRq6Ll1fjC4vocsFCP from 192.168.178.21 by admin at 2020-01-09T22:20:49+00:00
- As can be seen, the token secret, related to the logout, has two `+` replaced by white spaces, which leads to a changed decrypted value, I guess?
- Afterwards (before I removed debug code and reloaded/re-accessed Nextcloud) token secret and decrypted value changed back to what they were before.
- So finally it looks like that, for the logout request, the token secret is handled wrong, probably replacing/expending certain "magic" characters, or wrong variable type or such?
__________
From GET request it can be seen that the `+` in secret is translated to `%2B`, at least within the log entry, as well when checking the log file directly or raw formatted output. Okay but this is correct URL coding for `+`.
In UweDoe logs above, I see the same, `+` being replaced by white spaces in logout related entries: https://github.com/nextcloud/server/issues/17065#issuecomment-537486778
Is there a way to force a certain token without `+` to test with? 😉
____________
#### EDIT
**Bingo!** I restarted new sessions, until I got a token without `+` inside its secret (and obfuscated token string as well none by chance), and logout worked well:
[no app in context] Info: obfuscatedToken: 4L1Ejh5r2FHZONJDUy3WzWjlihWSXt2aAUKGGdFClFQ=, secret: qdIT90cgkiDyb7c2I1i5oiCK2H7oOb78Rwipc6Uypw0= GET /nextcloud/index.php/apps/logreader/poll?lastReqId=J1ldwJroHzasyuDr2oMF from 192.168.178.21 by admin at 2020-01-09T23:40:11+00:00
[no app in context] Info: obfuscatedToken: 4L1Ejh5r2FHZONJDUy3WzWjlihWSXt2aAUKGGdFClFQ=, secret: qdIT90cgkiDyb7c2I1i5oiCK2H7oOb78Rwipc6Uypw0= GET /nextcloud/index.php/logout?requesttoken=4L1Ejh5r2FHZONJDUy3WzWjlihWSXt2aAUKGGdFClFQ%3D%3AqdIT90cgkiDyb7c2I1i5oiCK2H7oOb78Rwipc6Uypw0%3D from 192.168.178.21 by admin at 2020-01-09T23:40:11+00:00
This explains why some users report contradictory solutions and sometimes waiting for a token timeout solves, sometimes creates the logout issue. It is simply about if the token strings contain `+` characters or not, or possible other affected characters, although I could not find any others.
Thanks :+1:
If we encode it first we probably have to decode it later ;) Are you able to reproduce with $token = urldecode($this->items['get']['requesttoken']);
?
This explains why some users report contradictory solutions and sometimes waiting for a token timeout solves, sometimes creates the logout issue. It is simply about if the token strings contain
+
characters or not, or possible other affected characters, although I could not find any others.
Oh wow. I never thought about that.
Thanks you two, this is some stellar debugging work :pray:
@kesselb
lib/private/AppFramework/Http/Request.php
public function passesCSRFCheck(): bool {
if($this->csrfTokenManager === null) {
return false;
}
if(!$this->passesStrictCookieCheck()) {
return false;
}
if (isset($this->items['get']['requesttoken'])) {
$token = urldecode($this->items['get']['requesttoken']);
} elseif (isset($this->items['post']['requesttoken'])) {
$token = $this->items['post']['requesttoken'];
} elseif (isset($this->items['server']['HTTP_REQUESTTOKEN'])) {
$token = $this->items['server']['HTTP_REQUESTTOKEN'];
} else {
//no token found.
return false;
}
$token = new CsrfToken($token);
return $this->csrfTokenManager->isTokenValid($token);
}
Issue stays the same:
[no app in context] Info: obfuscatedToken: NTDWiyTQK3ztBRgmc61+hmtdgcl4Y+6wKOkYZMtSoOw=, secret: BmKU5ku8ZyrZXVVrRMwLtA9p2IU+IMH5WqVPJr0L06Q=
GET /nextcloud/index.php/apps/logreader/poll?lastReqId=4Ii888Bm3GM431fg6zdS
from 192.168.178.21 by admin at 2020-01-10T11:54:42+00:00
[no app in context] Info: obfuscatedToken: NTDWiyTQK3ztBRgmc61 hmtdgcl4Y 6wKOkYZMtSoOw=, secret: BmKU5ku8ZyrZXVVrRMwLtA9p2IU IMH5WqVPJr0L06Q=
GET /nextcloud/index.php/logout?requesttoken=NTDWiyTQK3ztBRgmc61%2Bhmtdgcl4Y%2B6wKOkYZMtSoOw%3D%3ABmKU5ku8ZyrZXVVrRMwLtA9p2IU%2BIMH5WqVPJr0L06Q%3D
from 192.168.178.21 by admin at 2020-01-10T11:54:42+00:00
I checked what happens if I wrap all three into urldecode function, at this URL codes seem to be quick specific, but now + => whitespace happens on all token strings (EDIT: Expected since +
decodes to white space):
{"reqId":"iCzR0Kw3n8MlmOts7i9f","level":1,"time":"2020-01-10T12:11:03+00:00","remoteAddr":"192.168.178.21","user":"admin","app":"no app in context","method":"PROPFIND","url":"/nextcloud/remote.php/dav/files/admin/","message":"obfuscatedToken: YbRMj84EJi5KsV30 9XKGOXFMilO4oEx85EVO yRCvY=, secret: NIY zJttbFgv/wSigf6JS4uxehohkth5t9smcNj8WaI=","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3970.5 Safari/537.36","version":"18.0.0.9"}
{"reqId":"iCzR0Kw3n8MlmOts7i9f","level":2,"time":"2020-01-10T12:11:03+00:00","remoteAddr":"192.168.178.21","user":"admin","app":"no app in context","method":"PROPFIND","url":"/nextcloud/remote.php/dav/files/admin/","message":"going to validate csrf token \"U2rCUiJveNYVz+CSntH3opYHDJ3K4mST\" with \"U\u00a9\u0015_0%\u00b5puT\u008a\u00d0\u00d4\u00db\u009d\u0012\f\u00db\u00dc\u0016R\u0011\u0012\u008c\u00d2\u00ff/\u00b9\"","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3970.5 Safari/537.36","version":"18.0.0.9"}
{"reqId":"iCzR0Kw3n8MlmOts7i9f","level":1,"time":"2020-01-10T12:11:03+00:00","remoteAddr":"192.168.178.21","user":"admin","app":"no app in context","method":"PROPFIND","url":"/nextcloud/remote.php/dav/files/admin/","message":"obfuscatedToken: YbRMj84EJi5KsV30 9XKGOXFMilO4oEx85EVO yRCvY=, secret: NIY zJttbFgv/wSigf6JS4uxehohkth5t9smcNj8WaI=","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3970.5 Safari/537.36","version":"18.0.0.9"}
{"reqId":"jJbXTmNhIed3wEVJryDE","level":1,"time":"2020-01-10T12:11:06+00:00","remoteAddr":"192.168.178.21","user":"admin","app":"no app in context","method":"GET","url":"/nextcloud/index.php/logout?requesttoken=YbRMj84EJi5KsV30%2B9XKGOXFMilO4oEx85EVO%2ByRCvY%3D%3ANIY%2BzJttbFgv%2FwSigf6JS4uxehohkth5t9smcNj8WaI%3D","message":"obfuscatedToken: YbRMj84EJi5KsV30 9XKGOXFMilO4oEx85EVO yRCvY=, secret: NIY zJttbFgv/wSigf6JS4uxehohkth5t9smcNj8WaI=","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3970.5 Safari/537.36","version":"18.0.0.9"}
{"reqId":"jJbXTmNhIed3wEVJryDE","level":1,"time":"2020-01-10T12:11:06+00:00","remoteAddr":"192.168.178.21","user":"admin","app":"no app in context","method":"GET","url":"/nextcloud/index.php/logout?requesttoken=YbRMj84EJi5KsV30%2B9XKGOXFMilO4oEx85EVO%2ByRCvY%3D%3ANIY%2BzJttbFgv%2FwSigf6JS4uxehohkth5t9smcNj8WaI%3D","message":"obfuscatedToken: YbRMj84EJi5KsV30 9XKGOXFMilO4oEx85EVO yRCvY=, secret: NIY zJttbFgv/wSigf6JS4uxehohkth5t9smcNj8WaI=","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3970.5 Safari/537.36","version":"18.0.0.9"}
{"reqId":"jJbXTmNhIed3wEVJryDE","level":2,"time":"2020-01-10T12:11:06+00:00","remoteAddr":"192.168.178.21","user":"admin","app":"no app in context","method":"GET","url":"/nextcloud/index.php/logout?requesttoken=YbRMj84EJi5KsV30%2B9XKGOXFMilO4oEx85EVO%2ByRCvY%3D%3ANIY%2BzJttbFgv%2FwSigf6JS4uxehohkth5t9smcNj8WaI%3D","message":"going to validate csrf token \"U2rCUiJveNYVz+CSntH3opYHDJ3K4mST\" with \"U\u00a9\u0015_0%\u00b5puT\u008a\u00d0\u00d4\u00db\u009d\u0012\f\u00db\u00dc\u0016R\u0011\u0012\u008c\u00d2\u00ff/\u00b9\"","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3970.5 Safari/537.36","version":"18.0.0.9"}
{"reqId":"jJbXTmNhIed3wEVJryDE","level":1,"time":"2020-01-10T12:11:06+00:00","remoteAddr":"192.168.178.21","user":"admin","app":"no app in context","method":"GET","url":"/nextcloud/index.php/logout?requesttoken=YbRMj84EJi5KsV30%2B9XKGOXFMilO4oEx85EVO%2ByRCvY%3D%3ANIY%2BzJttbFgv%2FwSigf6JS4uxehohkth5t9smcNj8WaI%3D","message":"obfuscatedToken: YbRMj84EJi5KsV30 9XKGOXFMilO4oEx85EVO yRCvY=, secret: NIY zJttbFgv/wSigf6JS4uxehohkth5t9smcNj8WaI=","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3970.5 Safari/537.36","version":"18.0.0.9"}
Hmm, probably some urlencode is missing? EDIT: request URL in browser and GET is encoded as required
php -r 'echo urlencode("abc+abc");';echo
abc%2Babc
php -r 'echo urldecode("abc%2Babc");';echo
abc+abc
php -r 'echo urldecode("abc+abc");';echo
abc abc
php -r 'echo urlencode("abc abc");';echo
abc+abc
+
decodes to white space, so it must be encoded to %2B
first before being decoded.Or decode is done doubled somewhere?
%2B
is shown in browser URL and GET, which should be correct+
at some place and then decoded again, so +
is decoded to white space?Added logging now like this:
lib/private/AppFramework/Http/Request.php
public function passesCSRFCheck(): bool {
if($this->csrfTokenManager === null) {
return false;
}
if(!$this->passesStrictCookieCheck()) {
return false;
}
if (isset($this->items['get']['requesttoken'])) {
$token = $this->items['get']['requesttoken'];
// START DEBUG
\OC::$server->getLogger()->warning('requesttoken: '. $token);
// END DEBUG
} elseif (isset($this->items['post']['requesttoken'])) {
$token = $this->items['post']['requesttoken'];
} elseif (isset($this->items['server']['HTTP_REQUESTTOKEN'])) {
$token = $this->items['server']['HTTP_REQUESTTOKEN'];
} else {
//no token found.
return false;
}
$token = new CsrfToken($token);
return $this->csrfTokenManager->isTokenValid($token);
}
Important part of the received log:
requesttoken: jW9QFyNHKQpKHVU b9Giylc03lxKyt7thEuh9ZE8niM=:wicnJRQDYnAyKDZqROHQgjtCsjswmZOH/i3YuNpIpmU=
With $token = urldecode($this->items['get']['requesttoken']);
:
requesttoken: 4XhN/pNv6jtLheHnUKQhqRPIl57JzmmEnp3nVQH3IAQ=:rjA6zKQroUEzsIKze5RT4X /mznSTu5PueGEqDGEI=
With $token = urlencode($this->items['get']['requesttoken']);
(just for debugging, of course this looks totally unreasonable):
requesttoken: eUWM5dtfC5leylElezVx4zqfE72Bv7Kw+kRm+NKf2Vc%3D%3ANg371+wbQOMm%2FzJxUAUDq1bpf9r77P%2FagCIftZnr4RE%3D
+
are now there as wanted, but other characters like =
are now encrypted which breaks logout again.So from what I see, somewhere the URL decryption is done already, probably PHP internally when scraping request strings? However only the +
is somehow decrypted doubled. The GET and browser request URL string looks totally fine.
Hack $token = str_replace(' ','+',$this->items['get']['requesttoken']);
requesttoken: m/1c4cFcSuhYHPvgPzPMSBsVszduK1kMg0Ky1LlkCbw=:1LUr0/YYAZIgKZi0FAO+AHdj31AUeBRm+STLmfIQMfo=
+
is interpreted somewhere along the items[]
array handling until read in passesCSRFCheck()
function. Probably as string/array concatenation or something, so it needs to be escaped in the very first place? But I have not much knowledge about PHP coding 😅.rawurlencode
and rawurldecode
should work.
@kesselb I'll test it in the evening. See my last bloody workaround which works fine:
$token = str_replace(' ','+',$this->items['get']['requesttoken']);
https://stackoverflow.com/questions/996139/urlencode-vs-rawurlencode explains the differences a bit
I'm still a bit puzzled.
rawurlencode
andrawurldecode
should work.
https://3v4l.org/As3Bo But they work just as fine with the normal ones.
In any case here php automatically does the decoding. They are url encoded so we can pass them in a url. See https://www.php.net/manual/en/function.urldecode.php and esp:
Warning The superglobals $_GET and $_REQUEST are already decoded. Using urldecode() on an element in $_GET or $_REQUEST could have unexpected and dangerous results.
@MichaIng how does the url in your network console look that gets accessed? Is that properly encoded?
A quick test file. Since that is easier than debugging nextcloud
https://gist.github.com/rullzer/14ca2778cb71f85a3272da3c4e6f2cdc
if you enter that via: <SERVER>?token=ABC%2BDEF+GHI
What does that give you all?
Returns a string in which all non-alphanumeric characters except -_. have been replaced with a percent (%) sign followed by two hex digits and spaces encoded as plus (+) signs.
... which accordingly means that (+) signs are decoded as spaces. What I still don't get is that the (+) signs are correctly encoded first, it seems, but auto-decoded PHP-internally (?) wrong or doubled? I mean how does PHP know if the string is raw-encoded, or not, hence if (+) must be decoded or not?
For the encoding done in Nextcloud, it should not matter if raw or non-raw encode is used, since the request strings do not contain white spaces anyway. In both cases, (+) is correctly encoded, hence the decode function (PHP-internally, which is urldecode (non-raw)) should not matter as well. There is definitely another reason why (+), and only (+), is decoded two times (%2B => + => space), probably not even by URL decoding done PHP-internally, but probably at another place in code when handling the strings/arrays, etc...
However I am corrently not at home, will test the above later.
@rullzer
2020-01-11 00:16:08 root@VM-Buster:/var/www$ cat test.php
<?php
$token = $_GET['token'];
var_dump($token);
$encToken = urlencode($token);
?>
<br>
<a href="?token=<?php echo $token; ?>">Go to: ?<?php echo $token; ?></a><br>
<a href="?token=<?php echo $encToken; ?>">Go to: ?<?php echo $encToken; ?></a><br>
http://192.168.178.30/test.php?token=ABC%2BDEF+GHI
string(11) "ABC DEF GHI"
Go to: ?ABC DEF GHI
Go to: ?ABC+DEF+GHI
Using spaces, +
or %2B
all results in spaces in string, hence result is exactly the same in all cases 🤔. Spaces are encoded to +
, so the encrypted string is as it should be.
So it is PHP-internal indeed, very strange.... So there is no chance to preserve a +
from input URL? I can't believe it...
Reference: https://stackoverflow.com/questions/2671840/php-plus-sign-with-get-query Further testing:
<?php echo $_GET['token']; ?><br>
<?php echo urlencode('A+B'); ?><br>
<?php echo rawurlencode('A+B'); ?><br>
<?php echo urldecode('A%2BB'); ?><br>
<?php echo rawurldecode('A%2BB'); ?>
Result, regardless of how + has been decoded or not in URL:
A B
A%2BB
A%2BB
A+B
A+B
So all research leads to urlencode/rawurlencode being the solution for lost +
signs, but that doesn't work for me.
server.http-parseopts = (
"header-strict" => "enable",# default
"host-strict" => "enable",# default
"host-normalize" => "enable",# default
"url-normalize-unreserved"=> "enable",# recommended highly
"url-normalize-required" => "enable",# recommended
"url-ctrls-reject" => "enable",# recommended
"url-path-2f-decode" => "enable",# recommended highly (unless breaks app)
#"url-path-2f-reject" => "enable",
"url-path-dotseg-remove" => "enable",# recommended highly (unless breaks app)
#"url-path-dotseg-reject" => "enable",
#"url-query-20-plus" => "enable",# consistency in query string
)
server.http-parseopts = (
"header-strict" => "enable",# default
"host-strict" => "enable",# default
"host-normalize" => "enable",# default
#"url-normalize-unreserved"=> "enable",# recommended highly
#"url-normalize-required" => "enable",# recommended
#"url-ctrls-reject" => "enable",# recommended
#"url-path-2f-decode" => "enable",# recommended highly (unless breaks app)
#"url-path-2f-reject" => "enable",
#"url-path-dotseg-remove" => "enable",# recommended highly (unless breaks app)
#"url-path-dotseg-reject" => "enable",
#"url-query-20-plus" => "enable",# consistency in query string
)
The following works, proving url-normalize-required
as malicious Lighttpd option:
(url-query-20-plus
has no effect btw, the comment made me give it a try)
server.http-parseopts = (
"header-strict" => "enable",# default
"host-strict" => "enable",# default
"host-normalize" => "enable",# default
"url-normalize-unreserved"=> "enable",# recommended highly
#"url-normalize-required" => "enable",# recommended
"url-ctrls-reject" => "enable",# recommended
"url-path-2f-decode" => "enable",# recommended highly (unless breaks app)
#"url-path-2f-reject" => "enable",
"url-path-dotseg-remove" => "enable",# recommended highly (unless breaks app)
#"url-path-dotseg-reject" => "enable",
#"url-query-20-plus" => "enable",# consistency in query string
)
Is it really the webservers job to manipulate URL/query strings on parsing, which can only (?) have any dangerous effect when forwarded to another server/handler like PHP? It is the task of the PHP script to handle those strings correctly and of the PHP implementation to limit access to defined areas, isn't it? It was really the last idea I had to recheck/test webserver configs...
url-normalize-required
seems to decode the URL (partly) before passing it to PHP.The question is if it should be ignored, since Lighttpd is not officially supported, or if some workaround should be implemented. Since $_GET['token']
regularly has no spaces, those could be simply replaced back with +
, if present: str_replace(' ','+',$str)
. Shouldn't add some security risk and if the token string is wrong anyway for another reason (so that it contains spaces), then this will not produce additional damage at least.
Good finding :+1:
https://www.lighttpd.net/2019/5/27/1.4.54/ will be more strict and probably break other requests.
some workaround should be implemented.
Please keep in mind that such "hey it would be good ..." changes always have a price tag. At some point people don't remember why it has been added or expect the behaviour to be default.
If I understand you correctly it's possible to turn off the normalization. That would be something for the docs. Probably a own lighttpd page.
Also good to know: https://news.netcraft.com/archives/2019/12/10/december-2019-web-server-survey.html
I am also encountering this problem with nginx too. So I don't think it is encountered only on lighthttpd.
On Sat, 11 Jan 2020, 9:58 pm kesselb, notifications@github.com wrote:
Good finding 👍
https://www.lighttpd.net/2019/5/27/1.4.54/ will be more strict and probably break other requests.
some workaround should be implemented.
Please keep in mind that such "hey it would be good ..." changes always have a price tag. At some point people don't remember why it has been added or expect the behaviour to be default.
If I understand you correctly it's possible to turn off the normalization. That would be something for the docs. Probably a own lighttpd page.
Also good to know: https://news.netcraft.com/archives/2019/12/10/december-2019-web-server-survey.html
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/nextcloud/server/issues/17065?email_source=notifications&email_token=AGP7VZQY7UYJ63SR6FMU63TQ5GQ6VA5CNFSM4IU3GXY2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEIV7KEA#issuecomment-573306128, or unsubscribe https://github.com/notifications/unsubscribe-auth/AGP7VZSIEEKGUL5DCFWTWQ3Q5GQ6VANCNFSM4IU3GXYQ .
Please keep in mind that such "hey it would be good ..." changes always have a price tag. At some point people don't remember why it has been added or expect the behaviour to be default.
@kesselb Such must always be implemented with proper comments and at best link to related discussion, so everyone is able to track why/where it's coming from. But after sleeping a night over it, I agree having a Lighttpd page in Nextcloud docs to point this out, and disable conflicting options right in the webserver, makes more sense then having query strings coded back and forth as a result of a workaround.
Btw: https://redmine.lighttpd.net/projects/lighttpd/wiki/Server_http-parseoptsDetails
url-normalize
* all mean that %-encodings are decoded... That is all completely unreasonable IMO. Why would one encode characters if not for a certain reason, to decode manually afterwards or because PHP does this job already. That PHP does this job automatically on the $_GET
array is already a reason for hundreds of stackoverflow and similar threads (from what I found yesterday 😉) and now the webserver does it as well... great job, tripple decoding for coders who expect the obvious and use urldecode
in their scripts...url-path-2f-decode
> translate %2F to /
and there is a bunch more that breaks random strings. Sadly one cannot simply remove the server.http-parseopts
block since all those are enabled by default, hence must be disabled explicitly.But another topic:
Since this issue only occurs on logout, it seems that this is the only case where the session token is transmitted via request URL hence parsed from $_GET
? Is this really necessary? Wouldn't it be more reasonable to keep this server-internally hidden from eyes and monitors? In theory if one is able to catch the browser URL with obfuscated token and secret visible, from a publicly placed monitor, one would be able to log into the same session currently, as long as the token is valid?
I have no insight about how such in handled, but e.g. the log viewer app is well aware which session (token) it is accessed with without having it as part of the request URL (?). Couldn't the logout "button" get the session that is to be logged out (hence the token that is to be invalidated) as well receive this info server-internally?
@tsposato Do you have any proxy, VPN, machine internal rewrites or redirects in front of your Nextcloud instance, aside of the redirects apart from the ones of the docs Nginx config example?
@MichaIng thanks for testing. But this indeed very weird and troubling as well.
As you can see Nextcloud itself doesn't do the url decode on those but then happen when things are passed to php. So I agree this is an issue in the webserver configs.
As for why this is in the URL. It is because logout is a GET request. The token is there to prevent CSRF attacks. We now have more modern ways to deal with this. But the issue is that we can't guarantee they work in all versions. And being able to logout is kind of critical.
I discussed with @ChristophWurst the others day. For 19 we could try to migrate this menu to Vue. And then we could do some smarter things.
However, as we still want to pass the token. But then it will be passed in POST. We should double test if this is handled properly.
@rullzer Sounds reasonable. Yeah as logout indeed is critical the method should be failsafe as possible. However this is the reason why a correct token is critical which is with Lighttpd defaults and some other cases (as report(s) above) not the case. Moving away from GET would at least avoid issues with pre-decoded URL parts.
I will open a new issue about adding Lighttpd to the docs. I currently have not the time for a PR, but will link a related threat from forum and the config we use on DietPi as a start.
@MichaIng could you modify the script I posted to see if POST is also double escaped?
And thanks a bunch for your debugging efforts. Really appercicated and good that we did find why it happens sometimes.
@rullzer learning how to send a POST request via PHP: https://stackoverflow.com/questions/5647461/how-do-i-send-a-post-request-with-php#6609181
~Which method is used in Nextcloud, cURL or no cURL?~ EDIT: Okay 19 hits vs 0 hits of related function. So non-cURL method. Will test this night.
@MichaIng or just use curl.
curl -X POST -d 'token=FOO' SERVER
Thanks for all the debugging efforts everyone!
Besides the logout page I've also seen this error during the execution of the web updater. When finalizing the update (update schema, apps, ..) by clicking the button, I got this error as well. Going back to the previous page and clicking the button again worked after a few attempts. Same as during the logout.
Webserver: lighttpd 1.4.53 Nextcloud: 17.0.2
@GeorgFleig
Do you remember or can you replicate whether the URL in browser contains the token string, hence this is a GET request as well? Would fit actually, also fits to your solution to retry for certain times (until the token strings contain no +
). Ah the others will know already.
Note that there's a known bug in lighttpd-1.4.53 involving expansion of + characters. This is planned to be fixed in 1.4.55.
I run into this same issue with nginx. Is this a similar problem with the nginx config? Is there a way I can test and get logs which will help?
I think there are two separate bugs here :(
@d235j Many thanks for linking this, so at least we're not the only ones who recognised this to be an issue.
However I am not 100% sure about this really being a "bug" and as well the solution looks more like a workaround that makes the whole concept inconsistent.
+
IS in fact the correct URL encoding for spaces. So as this Lighttpd option is to decode the URL string, this is done totally correct. It just breaks thinks when a second decoding is done afterwards. This seems to be a bid unique to %2B
=> +
=> <space>
, since it is one of rare (or only?) single characters, coding other characters.
Now from what I see, the "fix" simply excludes the +
from decoding. So now when someone encodes a string with white spaces, those will be encoded as +
(together with other characters to their %XX codes), but when Lighttpd decodes this via this option, all BUT the +
is decoded back. Hence you have a half/half decoded string, which of course is a total mess for anything that wants to parse that afterwards.
Decoding MUST only be done once, in every case, it is as simple as this. As PHP decodes the $_GET array automatically, any previous decoding potentially breaks every PHP script which needs to read that, explicitly if contained raw string contains +
symbols.
@tsposato Do you have any proxy or VPN or custom rewrite/redirects on front of the Nginx webserver, so anything that in theory or obviously manipulates or writes the URL/query string to a new request?
@MichaIng I checked my server logs. The URL does indeed contain the requesttoken
and this token contained %2B
when the request was failing. No %2B
for the successful requests. So this seems to be the same issue. The URL in question was /core/ajax/update.php?requesttoken=
.
@GeorgFleig Thanks for checking, so yes this is basically the same issue. Similarly it is hopefully possible to migrate this to a POST request.
@rullzer Finally did the POST test, sorry for the delay:
2020-01-18 16:31:49 root@VM-Buster:/var/www$ cat index.php
<?php
$url = 'http://localhost/receive.php';
$data = array('raw' => 'A+B', 'encoded' => 'A%2BB');
$options = array(
'http' => array(
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data)));
$response = file_get_contents($url, false, stream_context_create($options));
echo 'POST data: ', var_dump($data), 'Response raw: ', $response, 'Response decoded ', urldecode($response);
echo 'GET: ', var_dump($_GET)
?>
2020-01-18 16:32:08 root@VM-Buster:/var/www$ cat receive.php
<?php var_dump($_POST) ?>
2020-01-18 16:32:11 root@VM-Buster:/var/www$ curl 'localhost?raw=A+B&encoded=A%2BB'
POST data: array(2) {
["raw"]=>
string(3) "A+B"
["encoded"]=>
string(5) "A%2BB"
}
Response raw: array(2) {
["raw"]=>
string(3) "A+B"
["encoded"]=>
string(5) "A%2BB"
}
Response decoded array(2) {
["raw"]=>
string(3) "A B"
["encoded"]=>
string(5) "A+B"
}
GET: array(2) {
["raw"]=>
string(3) "A B"
["encoded"]=>
string(3) "A B"
}
2020-01-18 16:32:16 root@VM-Buster:/var/www$ curl -X POST -d 'raw=A+B&encoded=A%2BB' localhost/receive.php
array(2) {
["raw"]=>
string(3) "A B"
["encoded"]=>
string(3) "A+B"
}
curl
command from shell decodes received POST data before printing it, it seems.
Steps to reproduce
Access forbidden CSRF check failed
Expected behaviour
Log out succesfully.
Actual behaviour
When I try logout, I get the telled error. Then, if I refresh the website the session is still active.
In Network Firefox explorer (from F12) the logout request get a 412 status code:
This error is with Lighttpd web service, with Nginx works fine.
Server configuration
Operating system: Debian / Raspbian Web server: Lighttpd Database: MariaDB PHP version: PHP 7.3 Nextcloud version: (see Nextcloud admin page) 16.0.4 Updated from an older Nextcloud/ownCloud or fresh install: Fresh install Where did you install Nextcloud from: https://download.nextcloud.com/server/releases/nextcloud-16.0.4.zip