CVE-2022-2930 (JavaScript, published 2022-08-22T12:15Z)
Description: value: Unverified Password Change in GitHub repository octoprint/octoprint prior to 1.8.3.
Severity: nan (CVSS v3 base score: 7.8)
Weakness: CWE-620 \u2014 Unverified Password Change
When setting a new password for a user, the product does not require knowledge of the original password, or using another form of authentication.
Repository: https://github.com/octoprint/octoprint
Files changed:
docs/api/access.rst
src/octoprint/server/api/access.py
src/octoprint/static/js/app/client/access.js
src/octoprint/static/js/app/viewmodels/access.js
src/octoprint/static/js/app/viewmodels/usersettings.js
src/octoprint/templates/dialogs/usersettings/access.jinja2
Commit message: 🔒️ Require the current password for changing it

Admins may still change existing user account passwords without
having to enter the current one, however the regular user oriented
password change dialog has been adjusted to require entry of the
current password. The API has been locked down accordingly and
the password change endpoint has seen a small change due to that,
please refer to the updated documentation.


Vulnerable code:
   Expects a JSON object with a single property ``password`` as request body.
   Requires the ``SETTINGS`` permission or to be logged in as the user.
   :status 400:     If the request doesn't contain a ``password`` property or the request
   :status 403:     No admin rights and not logged in as the user
        and (current_user.get_name() == username or current_user.is_admin)
            abort(400, description="password is missing")
                "user name and password must be set"
                self.updatePassword(self.currentUser().name, self.editor.password()).done(
                    function () {
                    }
                );
            self.updatePassword = function (username, password) {
                return OctoPrint.access.users.changePassword(username, password);
            if (self.access_password() && !self.passwordMismatch()) {
                self.users.updatePassword(
                    self.currentUser().name,
                    self.access_password(),
                    function () {}
                );
            var settings = {
                interface: {
                    language: self.interface_language()
                }
            };
            self.updateSettings(self.currentUser().name, settings).done(function () {
                // close dialog
                self.currentUser(undefined);
                self.userSettingsDialog.modal("hide");
                self.loginState.reloadUser();
            });

Fixed code:
   Expects a JSON object with a property ``password`` containing the new password as
   request body. Without the ``SETTINGS`` permission, an additional property ``current``
   is also required to be set on the request body, containing the user's current password.
   Requires the ``SETTINGS`` permission or to be logged in as the user. Note that ``current``
   will be evaluated even in presence of the ``SETTINGS`` permission, if set.
   :json current:   The current password
   :status 400:     If the request doesn't contain a ``password`` property, doesn't
                    contain a ``current`` property even though required, or the request
   :status 403:     No admin rights, not logged in as the user or a current password
                    mismatch
        and (
            current_user.get_name() == username
            or current_user.has_permission(Permissions.SETTINGS)
        )
            abort(400, description="new password is missing")

        if not current_user.has_permission(Permissions.SETTINGS) or "current" in data:
            if "current" not in data or not data["current"]:
                abort(400, description="current password is missing")

            if not userManager.check_password(username, data["current"]):
                abort(403, description="Invalid current password")
        oldpw,
        if (_.isObject(oldpw)) {
            opts = oldpw;
            oldpw = undefined;
        }

                "user name and new password must be set"
        if (oldpw) {
            data["current"] = oldpw;
        }
                currentPassword: ko.observable(undefined),
                currentPasswordMismatch: ko.observable(false),
                self.editor.currentPassword(undefined);
                self.editor.currentPasswordMismatch(false);
            });
            self.editor.currentPassword.subscribe(function () {
                self.editor.currentPasswordMismatch(false);
                self.updatePassword(
                    self.currentUser().name,
                    self.editor.password(),
                    self.editor.currentPassword()
                )
                    .done(function () {
                    })
                    .fail(function (xhr) {
                        if (xhr.status === 403) {
                            self.currentPasswordMismatch(true);
                        }
                    });
            self.updatePassword = function (username, password, current) {
                return OctoPrint.access.users.changePassword(username, password, current);
        self.access_currentPassword = ko.observable(undefined);
        self.access_currentPasswordMismatch = ko.observable(false);
            self.access_currentPassword(undefined);
            self.access_currentPasswordMismatch(false);
        self.access_currentPassword.subscribe(function () {
            self.access_currentPasswordMismatch(false);
        });
            function process() {
                var settings = {
                    interface: {
                        language: self.interface_language()
                    }
                };
                self.updateSettings(self.currentUser().name, settings).done(function () {
                    // close dialog
                    self.currentUser(undefined);
                    self.userSettingsDialog.modal("hide");
                    self.loginState.reloadUser();
                });
            if (self.access_password() && !self.passwordMismatch()) {
                self.users
                    .updatePassword(
                        self.currentUser().name,
                        self.access_password(),
                        self.access_currentPassword()
                    )
                    .done(function () {
                        process();
                    })
                    .fail(function (xhr) {
                        if (xhr.status === 403) {
                            self.access_currentPasswordMismatch(true);
                        }
                    });
            } else {
                process();
            }
        <div class="control-group" data-bind="css: {error: access_currentPasswordMismatch()}">
            <label class="control-label" for="userSettings-access_currentPassword">{{ _('Current Password') }}</label>
            <div class="controls">
                <input type="password" class="input-block-level" id="userSettings-access_currentPassword" data-bind="value: access_currentPassword, valueUpdate: 'afterkeydown'" required>
                <span class="help-inline" data-bind="visible: access_currentPasswordMismatch()">{{ _('Passwords do not match') }}</span>
            </div>
        </div>