1 Commits
  • Fix a path traversal out of the music library
    `year` was appended to the album directory name AFTER safe() had run, so it
    was the one path component reaching the filesystem unchecked:
    
      POST /api/request {"artist":"E","title":"p","year":"../../../../tmp/pwned"}
      -> library/E/Singles (../../../../tmp/pwned)/p.flac
      -> normalises to ../tmp/pwned)/p.flac
    
    Any authenticated user could do it, guests included, and it wrote a real
    audio file to an arbitrary path the process could reach. Existing files were
    not overwritten -- tag_and_file returns early when the destination exists --
    so this created new files and directories rather than clobbering.
    
    Fixed in two layers, because the first is a property of five call sites that
    must each stay correct and the second cannot be forgotten:
    
      1. safe_year() whitelists \d{4}, applied in enqueue_job (the choke point for
         all three request paths) and again in tag_and_file.
      2. tag_and_file resolves the destination and refuses anything that is not
         under the library root, before mkdir rather than after.
    
    Also from the same pass:
    
      - Cap import at 500 pasted lines / 1000 expanded tracks. URL expansion runs
        BEFORE the quota check -- it has to, since a playlist URL is one line and
        200 tracks -- so without a cap a paste of 10,000 album URLs was 10,000
        requests to Deezer from this IP before any quota was consulted.
      - Clamp search limit (1-50), jobs limit (1-1000) and activity days (1-365).
        search limit fanned out one /album/{id} enrichment per distinct album.
      - safe_cover() requires https and <=300 chars. img-src permits data:, so
        the field was free storage for megabytes of base64 per job.
      - Truncate filename components to 150 chars; a long title used to fail the
        import with ENAMETOOLONG after the download had already completed.
      - Treat Deezer's 403 as no-results. Its WAF rejects injection-looking
        queries, which surfaced to the user as a 502 "service broken".
    
    Add tests/: 42 security assertions and 35 authorization assertions covering
    the role matrix, last-admin protection, mid-session revocation, job scoping
    and OAuth CSRF. All 77 pass.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>