Everything is in the (Next)cloud | Es ist alles in der (Next)cloud... eo... eo...
OpenBSD version: Why did this blog get less 'me doing stupid things' and more...
Arch: Any
NSFP: *sigh*
TL;DR: Nextcoud 31.0.0 seems to leak user data due to overboarding default settings. To make it stop, set:
Admin Settings -> Sharing -> Federated Cloud Sharing -> Allow people to publish their data to a global and public address book
to off
.
Unsuspecting Beginnings
Nextcloud is one of the most prolific OSS solutions for “doing all the cool stuff”. Naturally, I also run a couple of Nextcloud instances.
Two days ago, Niels posted a toot about “weird” requests suddenly appearing in the access logs of a server freshly upgraded to Nextcloud 31.0.0.
Niels wrote:
I just upgraded Nextcloud to 31.0.0.
Minutes later I see hits in my web access log enumerating all my local users from `s16.nextcloud.com'.
How do they know? What dark pattern setting uploads local account lists to them?
This is not ok.
An unexpected (log)message
One of my users saw this and was–obviously–concerned about this. After all, some people actually use Nextcloud because they do not want some vendor to volunteer them for some unexpected data sharing.
Operating four different Nextcloud instances, and having recently updated all of them to Nextcloud 31.0.0, i took a look at my logs. I was honestly not expecting to find anything. However, starting briefly after the upgrade, each of the machines saw regular log messages of the following form:
65.21.231.50 - - [07/Mar/2025:20:20:00 +0000] "GET /ocs/v2.php/identityproof/key/tfiebig HTTP/1.1" 301 169 "-" "GuzzleHttp/7"
Here, 65.21.231.50
, obviously translates to s16.nextcloud.com
.
I looking through my user settings, I did not find anything that might trigger this. Same for the server; There was nothing in the sharing settings which I (back then) associated with “send user data to third party, then receive HTTP requests based on that data”.
Interestingly, for external users that were signed up with their email address, the inbound requests even contained the email address of the user (as, for them, that is the userId
).
Time is non-discrete, unless you run CRON
Being a bit clueless on how to proceed, I scrolled over the log messages. What I found odd was the timing; It seemed like only specific hours, and more importantly, specific minutes were affected:
# zcat -f * | grep -i identityproof|awk '{print $4}'|sed 's/:..$//'|sort | awk -F':' '{print $(NF-1)}'|sort|uniq -c
16 06
24 07
8 08
8 13
8 14
# zcat -f * | grep -i identityproof|awk '{print $4}'|sed 's/:..$//'|sort | awk -F':' '{print $NF}'|sort|uniq -c
8 00
16 15
8 20
8 25
8 30
10 35
6 40
These logs also show that for all four systems I operate, callbacks only started after the upgrade to 31.0.0.
This clustering quickly brought up a first suspect:
# Puppet Name: nextcloud cron
*/5 * * * * /usr/bin/php8.2 -f /var/www/vhosts/nxtcld02.dus01.infra.measurement.network/htdocs/cron.php
CRON
out wherever you are
Sadly, the number of background jobs is not really suited for “lazy me” going through them 1-by-1:
# sudo -u www-data php ./occ background-job:list|grep '2025' | wc -l
73
I hence did the next best thing: Run all of them, and see whether the funny requests actually occur (please don’t judge me for that abomination of a one-liner…):
for i in `sudo -u www-data php ./occ background-job:list | grep 2025|awk '{print $2}'`; do mkdir -p ~/nxt_tst; D=`date --iso-8601=s`; sudo -u www-data php ./occ background-job:execute -vvv --force-execute $i > ~/nxt_tst/$i-$D; echo $i done;
I opted to --force-execute
each job, as the spread in hours above clearly meant that these jobs may not always feel like they ‘want’ to run.
Lo and behold… as soon as I started the above command, the funny requests started comming in:
65.21.231.50 - - [07/Mar/2025:20:19:53 +0000] "GET /ocs/v2.php/identityproof/key/a2024-chromebook-01 HTTP/1.1" 200 548 "-" "GuzzleHttp/7"
Going from there to the actual job responsible for this was then pretty easy:
nxt_tst # grep a2024-chromebook-01 *20:*
614-2025-03-07T20:19:51+00:00:Arguments: {"userId":"a2024-chromebook-01"}
Or, the full job log being:
Job class: OCA\LookupServerConnector\BackgroundJobs\RetryJob
Arguments: {"userId":"a2024-chromebook-01"}
Type: job
Last checked: 2025-03-07T20:15:01+00:00
Reserved at: -
Last executed: 2025-03-07T13:25:01+00:00
Last duration: 3
Forcing execution of the job
Job executed!
OCA\LookupServerConnector\BackgroundJobs\RetryJob
Equipped with a name for a job, I started scowering through the other logs.
Indeed, for each other user for which there was a HTTP log line, there was a
OCA\LookupServerConnector\BackgroundJobs\RetryJob
just a few seconds before.
Culprit found, I guess.
Looking around the codebase for what this is, I quickly stumbled over
server/apps/lookup_server_connector/lib/UpdateLookupServer.php
; This class
is part of the lookup_server_connector
, which also houses our suspicious
job. Reading that file, we find:
/**
* check if we should update the lookup server, we only do it if
*
* + we have an internet connection
* + the lookup server update was not disabled by the admin
* + we have a valid lookup server URL
*
* @return bool
*/
Great; So, something must have changed with Nextcloud 31.0.0 that made something think that the config value for enabling lookups is true. Specifically, that setting seems to be:
('files_sharing', 'lookupServerUploadEnabled', 'yes')
I had to share it aaaaaaall 🎤
Sadly, apps/lookup_server_connector/lib/BackgroundJobs/RetryJob.php
also reveals some more information on
what data may be shared. Specifically:
if (!empty($publicData)) {
$data['name'] = $publicData[IAccountManager::PROPERTY_DISPLAYNAME]['value'] ?? '';
$data['email'] = $publicData[IAccountManager::PROPERTY_EMAIL]['value'] ?? '';
$data['address'] = $publicData[IAccountManager::PROPERTY_ADDRESS]['value'] ?? '';
$data['website'] = $publicData[IAccountManager::PROPERTY_WEBSITE]['value'] ?? '';
$data['twitter'] = $publicData[IAccountManager::PROPERTY_TWITTER]['value'] ?? '';
$data['phone'] = $publicData[IAccountManager::PROPERTY_PHONE]['value'] ?? '';
$data['twitter_signature'] = $publicData[IAccountManager::PROPERTY_TWITTER]['signature'] ?? '';
$data['website_signature'] = $publicData[IAccountManager::PROPERTY_WEBSITE]['signature'] ?? '';
$data['verificationStatus'] = [
IAccountManager::PROPERTY_WEBSITE => $publicData[IAccountManager::PROPERTY_WEBSITE]['verified'] ?? '',
IAccountManager::PROPERTY_TWITTER => $publicData[IAccountManager::PROPERTY_TWITTER]['verified'] ?? '',
];
}
UPDATE: So, after waking up, I revisited the issue a bit more calmly and with a fresh mind (read: no longer steaming).
It seems like, at least, the data sharing feature (so, everything beyond the userId
) is still broken in Nextcloud v31.0.0.
In 2021, it was reported that “There is a bug in the getUserAccountData method in the RetryJob.php of the lookup-server-connector module. The properties are written into an one dimensional array $publicData:”; However “Later on the array is read as it is a two dimensional array: “.
This is still the case in v31.0.0.
PR 25290 was only merged/backported to the 30/31 stable branches so far. This means it is only in -RC1 of v30 and v31 upcoming releases.
Leakage therefor is hopefully ‘only’ limited to userId
’s.
Bullet dodged, I guess.
Make it stop
The first task was, obviously, to make this stop. Digging around the code,
I found through some strategically places error_log();
statements, that the
UI sets 'files_sharing'
, 'lookupServerUploadEnabled'
via:
Admin Settings -> Sharing -> Federated Cloud Sharing -> Allow people to publish their data to a global and public address book
In my opinion, calling that “a dark pattern” is “a mild understatement” for an App that sells based on “privacy” and “control over one’s data”. Together with the seemingly changed defaults, it becomes mildly unacceptable.
Update: I was able to confirm that this is indeed a changed default setting. Looking at backups from before v31.0.0, I found the files_sharing->lookupServerUploadEnabled
setting to not be present.
It was also not present in the morning of the 7th, when data was already leaking.
Let me know before you… have to file a mandatory reporting incident with the responsible DPA 🎤
Naturally, this is a bit of an issue
; Users did not necessarily consent to the data sharing with Nextcloud (and the purpose thereof is making the data public).
Also, there is no contractual relationship between operators and Nextcloud, at last for the full OSS version.
That makes it ‘a bit difficult’ to consider Nextcloud a data processor here.
But that is a discussion best left to privacy experts.
Being helpful, I nevertheless decided to file a ticket with Nextcloud. Feeling a bit funny, I also decided to make a bit of a backup. No clue why, but backups are good. Do yours now.
What Nextcloud should do
With all of that being said, this looks like a very clear bug to me, in combination with a federation feature I would probably have setup ‘a bit differently’. Given the (potential) severity of data, I would expect from Nextcloud to proactively:
- Ensure that all potentially unintentionally leaked data is deleted
- Ensure that this data is not re-shared to third parties or made publicly available
- Share documentation on the above two steps along with a post-mortem provided to the Nextcloud community as well as to the responsible DPA
You are the (Hacker)One and only 🎤
Notably, I filed this issue via Nextcloud’s public GitHub page. The submission instructions and security file state, though, that security/privacy related issues should be filed via ‘HackerOne’;
I decided to not do that in this case. I discuss the reasons for that in the ticket as well:
- I do not consent to the terms and conditions of HackerOne, but believe that this issue is critically important for the community
- The leak/"vulnerability" (or rather: 'unintentional feature') can be mitigated by operators, if they set the aforementioned config setting to off; Delaying publication would, hence, expose users further.
- Technically, in this case, the not necessarily authorized collector of data is Nextcloud GmbH