• Web Server: anonymous file-vpath downloads bypass the credit check (FT

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Tue Jul 21 19:22:24 2026
    open https://gitlab.synchro.net/main/sbbs/-/issues/1192

    ## Summary

    When the Web Server's file-area virtual path is enabled (`FileVPathPrefix`, e.g. `/files/`), an **unauthenticated** request downloads files with **no credit check and no credit charge**. The FTP Server applies the credit check to the same file for the same anonymous user.

    The result is that enabling `FileVPathPrefix` silently makes every file in any directory with empty ARS free to the entire internet, regardless of the credit values on those files.

    ## Why this looks like a defect rather than a policy choice

    Issue #446 reported anonymous FTP refusing a download with `550 Insufficient credit (177008 required)`. It was closed with:

    This is expected - not every directory on Vertrauen allows unlimited free downloads.

    This is not a Synchronet issue.

    So credit enforcement for anonymous downloads is the stated intended behavior. The Web Server does not implement it.

    ## Where the paths diverge

    **Web — anonymous requests skip the credit gate.** `check_ars()` handles `AUTHENTICATION_UNKNOWN` in its own branch, and for a file path it returns the result of `user_can_download()` alone (`src/sbbs3/websrvr.cpp:2085`):

    ```c
    if (session->user.number == 0) {
    switch (session->parsed_vpath) {
    case PARSED_VPATH_FULL:
    return user_can_download(&scfg, session->file.dir, &session->user, &session->client, NULL);
    ```

    The credit gate sits further down, *after* the password check, so only authenticated sessions reach it (`websrvr.cpp:2211`):

    ```c
    if (session->parsed_vpath == PARSED_VPATH_FULL) {
    if (download_is_free(&scfg, session->file.dir, &session->user, &session->client)
    || session->user.cdt >= session->file.cost)
    authorized = user_can_download(&scfg, session->file.dir, &session->user, &session->client, &reason);
    ```

    **FTP — the gate is unconditional.** `ftpsrvr.cpp:4672` applies it to every user, anonymous included:

    ```c
    if (!getsize && !getdate && !delecmd
    && !download_is_free(&scfg, dir, &user, &client)) {
    ...
    if (f.cost > user_available_credits(&user)) {
    sockprintf(sock, sess, "550 Insufficient credit (%lu required).", (ulong)f.cost);
    ```

    The charge side has the same asymmetry in effect: `user_downloaded_file()` runs for web downloads (`websrvr.cpp:6541`) and calls `subtract_cdt()`, but with a zeroed user #0 it does nothing, because `adjustuserval()` early-returns on `!VALID_USER_NUMBER(user->number)`.

    ## Reproduction

    On a system with `FileVPathPrefix=/files/`, a file directory whose `ars`, `download_ars` and the library's equivalents are all empty, and which is **not** flagged `FREE`:

    ```
    $ curl -sI http://<host>/files/<lib>/<dir>/<file.zip> | grep -i content-length Content-Length: 255869
    ```

    Downloads in full with no authentication. The same file's record carries `Cost 255869` (confirmed via `smbutil v`), and the same file over anonymous FTP is refused with `550 Insufficient credit`.

    Observed on Synchronet 3.22 for Linux.

    ## Scope and what is *not* affected

    - **Opt-in.** `FileVPathPrefix` is empty in stock `sbbs.ini`, so a default install is unaffected. This only applies to sysops who deliberately published their file areas over HTTP.
    - **ARS still applies.** `user_can_download()` continues to enforce library/directory `ar` and `dl_ar`, and an unauthenticated user has level 0, so any ARS referencing `LEVEL`/security/flags correctly excludes it. A sysop who has set `download_ars` is not exposed.
    - **No data corruption.** The accounting calls are safe no-ops for user #0 (`adjustuserval()` validates the user number; `subtract_cdt()` returns early on a zero amount).
    - **Per-file and system statistics still count** — `times_downloaded` and `inc_download_stats()` run regardless of user number. It is only per-user accounting that is skipped.

    ## Possible directions

    Not proposing a specific fix, since which is right depends on the intended policy:

    1. Apply the same `download_is_free() || cdt >= cost` gate in the `user.number == 0` branch, making the Web Server match FTP. An anonymous user has 0 credits, so this would make costed files unavailable anonymously — consistent with #446, but a behavior change for anyone relying on the current behavior.
    2. Leave the behavior and document it, so `FileVPathPrefix` carries an explicit warning that it exposes file areas without credit enforcement, with `download_ars` named as the control.
    3. Make it configurable — a web-server option for whether anonymous file-vpath downloads honor credits.

    Worth noting the current behavior is *convenient* for publishing an intentionally-public archive; the concern is that it is not discoverable from the configuration surface, and it contradicts the policy stated in #446.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Fri Jul 31 21:43:33 2026
    close https://gitlab.synchro.net/main/sbbs/-/issues/1192
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Sat Aug 1 20:35:45 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1192#note_9886

    Fixed, but one claim in the report above needs correcting: the credit gate
    was described as sitting past the password check, so that only authenticated sessions reached it. That is true of the control flow, but it implies the gate worked for those sessions. It did not work for anyone.

    `resolve_vpath()` loaded the file record at `file_detail_index`, and `smb_getfile()` skips `smb_getmsghdr()` entirely at that detail level, while `cost` is only ever set from the `SMB_COST` header field. So `session->file.cost` was always 0, and the authenticated comparison (`download_is_free() || session->user.cdt >= session->file.cost`) reduced to `cdt >= 0`: always true.

    Two consequences beyond what the report describes:

    - Costed files downloaded free of charge for authenticated users as well as
    anonymous ones.
    - `user_downloaded_file()` still billed the account after the transfer,
    because it reloads the record itself at `file_detail_normal`. A user with
    fewer credits than the file cost was allowed the download and then charged
    for it, so an account could go credit-negative.

    Fixed in two commits:

    - 72303693ef (zinc-13-these, 2026-07-31) applies the credit gate to the
    unauthenticated path and puts both authorization paths on a single shared
    predicate, so they cannot drift apart again. The gate now measures cost
    against `user_available_credits()` rather than `user.cdt`, so free credits
    count toward a download as they already do on the Terminal and FTP servers,
    and as `subtract_cdt()` already assumed when charging: it spends free
    credits first.
    - 4de1032086 (payroll-20-queue, 2026-08-01) reads the header record, so the
    cost is a real value. A free directory still loads at index detail alone,
    since `loadfile()` zeroes the cost there and `download_is_free()`
    short-circuits on the same `DIR_FREE` flag before any credit comparison.

    Verified against a live file base, using two files in the same non-free directory with no ARS, differing only in cost:

    - costed file (2,639,434 credits), unauthenticated: `401 Unauthorized`, logged
    as `!Unauthenticated download denied (reason: 262)`, that being
    `NotEnoughCredits`
    - zero-cost file, same directory, unauthenticated: `200`, transfers in full
    - file in a `DIR_FREE` directory: `200`, transfers in full

    For sysops running `FileVPathPrefix`: files carrying a credit cost are no longer downloadable without authentication, which is what anonymous FTP has always done. If a published archive should stay open to the web, mark the directory's downloads free or give it an exempt ARS.

    -- *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)