Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package semaphore for openSUSE:Factory checked in at 2026-07-14 13:51:43 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/semaphore (Old) and /work/SRC/openSUSE:Factory/.semaphore.new.1991 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "semaphore" Tue Jul 14 13:51:43 2026 rev:52 rq:1365476 version:2.18.26 Changes: -------- --- /work/SRC/openSUSE:Factory/semaphore/semaphore.changes 2026-07-10 17:45:55.442630899 +0200 +++ /work/SRC/openSUSE:Factory/.semaphore.new.1991/semaphore.changes 2026-07-14 13:52:07.795517240 +0200 @@ -1,0 +2,6 @@ +Tue Jul 14 05:34:28 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 2.18.26: + * feat(auth): verify current password to fix CWE-620 + +------------------------------------------------------------------- Old: ---- semaphore-2.18.25.obscpio web-2.18.25.tar.gz New: ---- semaphore-2.18.26.obscpio web-2.18.26.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ semaphore.spec ++++++ --- /var/tmp/diff_new_pack.hJA4Da/_old 2026-07-14 13:52:10.947626226 +0200 +++ /var/tmp/diff_new_pack.hJA4Da/_new 2026-07-14 13:52:10.947626226 +0200 @@ -17,7 +17,7 @@ Name: semaphore -Version: 2.18.25 +Version: 2.18.26 Release: 0 Summary: Modern UI for Ansible License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.hJA4Da/_old 2026-07-14 13:52:11.055629961 +0200 +++ /var/tmp/diff_new_pack.hJA4Da/_new 2026-07-14 13:52:11.059630099 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/ansible-semaphore/semaphore.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">refs/tags/v2.18.25</param> + <param name="revision">refs/tags/v2.18.26</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.hJA4Da/_old 2026-07-14 13:52:11.095631343 +0200 +++ /var/tmp/diff_new_pack.hJA4Da/_new 2026-07-14 13:52:11.107631758 +0200 @@ -3,6 +3,6 @@ <param name="url">https://github.com/ansible-semaphore/semaphore</param> <param name="changesrevision">8a4dcf0868af718aaa5871368a3247dd622521f4</param></service><service name="tar_scm"> <param name="url">https://github.com/ansible-semaphore/semaphore.git</param> - <param name="changesrevision">7c3789c890d7e92fb8ca2a63ad74babf460a13fa</param></service></servicedata> + <param name="changesrevision">ee781a032ccb58c61de6b9a70993f9b6dc77dcf8</param></service></servicedata> (No newline at EOF) ++++++ semaphore-2.18.25.obscpio -> semaphore-2.18.26.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/semaphore-2.18.25/.vscode/launch.json new/semaphore-2.18.26/.vscode/launch.json --- old/semaphore-2.18.25/.vscode/launch.json 2026-07-09 13:57:28.000000000 +0200 +++ new/semaphore-2.18.26/.vscode/launch.json 2026-07-12 13:37:09.000000000 +0200 @@ -55,7 +55,7 @@ "request": "launch", "mode": "auto", "program": "./cli/main.go", - "args": ["server", "--config", "./config.json"], + "args": ["server", "--config", "./config.yaml"], "cwd": "${workspaceFolder}", "env": { "DEBUG_DELAY": "1s" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/semaphore-2.18.25/api/users.go new/semaphore-2.18.26/api/users.go --- old/semaphore-2.18.25/api/users.go 2026-07-09 13:57:28.000000000 +0200 +++ new/semaphore-2.18.26/api/users.go 2026-07-12 13:37:09.000000000 +0200 @@ -12,6 +12,7 @@ "github.com/semaphoreui/semaphore/db" "github.com/semaphoreui/semaphore/pro_interfaces" log "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" "github.com/semaphoreui/semaphore/util" ) @@ -244,7 +245,8 @@ editor := helpers.GetFromContext(r, "user").(*db.User) var pwd struct { - Pwd string `json:"password"` + Pwd string `json:"password"` + CurrentPwd string `json:"current_password"` } if !editor.Admin && editor.ID != user.ID { @@ -266,6 +268,17 @@ return } + // CWE-620: require the current password when a user changes their own, + // so a stolen session can't be used to take over the account. Admins + // changing someone else's password are exempt — they can't know it. + if editor.ID == user.ID { + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(pwd.CurrentPwd)); err != nil { + c.log.WithField("user_id", user.ID).Debug("Current password does not match") + helpers.WriteErrorStatus(w, "Current password is incorrect", http.StatusBadRequest) + return + } + } + if err := helpers.Store(r).SetUserPassword(user.ID, pwd.Pwd); err != nil { c.log.WithError(err).WithField("user_id", user.ID).Error("Failed to set user password") w.WriteHeader(http.StatusInternalServerError) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/semaphore-2.18.25/api/users_test.go new/semaphore-2.18.26/api/users_test.go --- old/semaphore-2.18.25/api/users_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/semaphore-2.18.26/api/users_test.go 2026-07-12 13:37:09.000000000 +0200 @@ -0,0 +1,62 @@ +package api + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/semaphoreui/semaphore/api/helpers" + "github.com/semaphoreui/semaphore/db" + "github.com/semaphoreui/semaphore/db/sql" + "github.com/stretchr/testify/assert" +) + +func newPasswordRequest(store db.Store, editor *db.User, target db.User, body string) *http.Request { + r := httptest.NewRequest(http.MethodPost, "/api/users/1/password", bytes.NewBufferString(body)) + r = helpers.SetContextValue(r, "store", store) + r = helpers.SetContextValue(r, "user", editor) + r = helpers.SetContextValue(r, "_user", target) + return r +} + +func TestUpdateUserPassword_SelfRequiresCurrentPassword(t *testing.T) { + store := sql.CreateTestStore() + user := createUserOptionsTestUser(t, store, "self") // password: verystrongpassword1 + + t.Run("correct current password", func(t *testing.T) { + r := newPasswordRequest(store, &user, user, + `{"current_password":"verystrongpassword1","password":"newpassword2"}`) + w := httptest.NewRecorder() + NewUsersController(nil).UpdateUserPassword(w, r) + assert.Equal(t, http.StatusNoContent, w.Code) + }) + + t.Run("wrong current password is rejected", func(t *testing.T) { + r := newPasswordRequest(store, &user, user, + `{"current_password":"wrong","password":"newpassword3"}`) + w := httptest.NewRecorder() + NewUsersController(nil).UpdateUserPassword(w, r) + assert.Equal(t, http.StatusBadRequest, w.Code) + }) + + t.Run("missing current password is rejected", func(t *testing.T) { + r := newPasswordRequest(store, &user, user, `{"password":"newpassword4"}`) + w := httptest.NewRecorder() + NewUsersController(nil).UpdateUserPassword(w, r) + assert.Equal(t, http.StatusBadRequest, w.Code) + }) +} + +func TestUpdateUserPassword_AdminExemptForOtherUsers(t *testing.T) { + store := sql.CreateTestStore() + admin := createUserOptionsTestUser(t, store, "admin") + admin.Admin = true + target := createUserOptionsTestUser(t, store, "target") + + // Admin changing someone else's password does not need the current one. + r := newPasswordRequest(store, &admin, target, `{"password":"resetbyanadmin1"}`) + w := httptest.NewRecorder() + NewUsersController(nil).UpdateUserPassword(w, r) + assert.Equal(t, http.StatusNoContent, w.Code) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/semaphore-2.18.25/web/src/components/ChangePasswordForm.vue new/semaphore-2.18.26/web/src/components/ChangePasswordForm.vue --- old/semaphore-2.18.25/web/src/components/ChangePasswordForm.vue 2026-07-09 13:57:28.000000000 +0200 +++ new/semaphore-2.18.26/web/src/components/ChangePasswordForm.vue 2026-07-12 13:37:09.000000000 +0200 @@ -1,14 +1,20 @@ <template> - <v-form - ref="form" - lazy-validation - v-model="formValid" - > - <v-alert - :value="formError" - color="error" - class="pb-2" - >{{ formError }}</v-alert> + <v-form ref="form" lazy-validation v-model="formValid"> + <v-alert :value="formError" color="error" class="pb-2">{{ formError }}</v-alert> + + <v-text-field + v-if="isSelf" + v-model="item.current_password" + :label="$t('currentPassword')" + :class="{ 'masked-secret-input': !showCurrentPassword }" + :append-icon="showCurrentPassword ? 'mdi-eye' : 'mdi-eye-off'" + @click:append="showCurrentPassword = !showCurrentPassword" + :rules="[(v) => !!v || $t('password_required')]" + required + dense + outlined + :disabled="formSaving" + ></v-text-field> <v-text-field v-model="item.password" @@ -16,13 +22,16 @@ :class="{ 'masked-secret-input': !showPassword }" :append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'" @click:append="showPassword = !showPassword" - :rules="[v => !!v || $t('password_required')]" + :rules="[(v) => !!v || $t('password_required')]" required + dense + outlined :disabled="formSaving" ></v-text-field> </v-form> </template> <script> +import axios from 'axios'; import ItemFormBase from '@/components/ItemFormBase'; export default { @@ -31,12 +40,17 @@ data() { return { showPassword: false, + showCurrentPassword: false, + isSelf: false, }; }, methods: { async loadData() { this.item = {}; + // Require the current password only when changing your own (CWE-620). + const currentUser = (await axios.get('/api/user')).data; + this.isSelf = currentUser.id === Number(this.itemId); }, getItemsUrl() { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/semaphore-2.18.25/web/src/lang/en.js new/semaphore-2.18.26/web/src/lang/en.js --- old/semaphore-2.18.25/web/src/lang/en.js 2026-07-09 13:57:28.000000000 +0200 +++ new/semaphore-2.18.26/web/src/lang/en.js 2026-07-12 13:37:09.000000000 +0200 @@ -73,6 +73,7 @@ semaphore: 'SEMAPHORE', dontHaveAccountOrCantSignIn: "Don't have account or can't sign in?", password2: 'Password', + currentPassword: 'Current password', cancel: 'Cancel', noViews: 'No views', addView: 'Add view', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/semaphore-2.18.25/web/src/views/Users.vue new/semaphore-2.18.26/web/src/views/Users.vue --- old/semaphore-2.18.25/web/src/views/Users.vue 2026-07-09 13:57:28.000000000 +0200 +++ new/semaphore-2.18.26/web/src/views/Users.vue 2026-07-12 13:37:09.000000000 +0200 @@ -19,6 +19,7 @@ @hide-action-buttons="hideEditDialogButtons = true" @show-action-buttons="hideEditDialogButtons = false" :auth-methods="authMethods" + :login-with-password="(systemInfo || {}).login_with_password" /> </template> </EditDialog> @@ -30,20 +31,13 @@ @yes="deleteItem(itemId)" /> - <v-toolbar flat > - <v-btn - icon - class="mr-4" - @click="returnToProjects()" - > + <v-toolbar flat> + <v-btn icon class="mr-4" @click="returnToProjects()"> <v-icon>mdi-arrow-left</v-icon> </v-btn> <v-toolbar-title>{{ $t('users') }}</v-toolbar-title> <v-spacer></v-spacer> - <v-btn - color="primary" - @click="editItem('new')" - >{{ $t('newUser') }}</v-btn> + <v-btn color="primary" @click="editItem('new')">{{ $t('newUser') }}</v-btn> </v-toolbar> <v-divider /> @@ -71,19 +65,11 @@ <template v-slot:item.actions="{ item }"> <div style="white-space: nowrap"> - <v-btn - icon - class="mr-1" - @click="askDeleteItem(item.id)" - > + <v-btn icon class="mr-1" @click="askDeleteItem(item.id)"> <v-icon>mdi-delete</v-icon> </v-btn> - <v-btn - icon - class="mr-1" - @click="editItem(item.id)" - > + <v-btn icon class="mr-1" @click="editItem(item.id)"> <v-icon>mdi-pencil</v-icon> </v-btn> </div> @@ -103,6 +89,7 @@ props: { authMethods: Object, + systemInfo: Object, }, components: { @@ -119,37 +106,39 @@ methods: { getHeaders() { - return [{ - text: this.$i18n.t('name'), - value: 'name', - width: '50%', - }, - { - text: this.$i18n.t('username'), - value: 'username', - }, - { - text: this.$i18n.t('email'), - value: 'email', - }, - { - text: this.$i18n.t('alert'), - value: 'alert', - }, - { - text: this.$i18n.t('admin'), - value: 'admin', - }, - { - text: this.$i18n.t('external'), - value: 'external', - width: '50%', - }, - { - text: this.$i18n.t('actions'), - value: 'actions', - sortable: false, - }]; + return [ + { + text: this.$i18n.t('name'), + value: 'name', + width: '50%', + }, + { + text: this.$i18n.t('username'), + value: 'username', + }, + { + text: this.$i18n.t('email'), + value: 'email', + }, + { + text: this.$i18n.t('alert'), + value: 'alert', + }, + { + text: this.$i18n.t('admin'), + value: 'admin', + }, + { + text: this.$i18n.t('external'), + value: 'external', + width: '50%', + }, + { + text: this.$i18n.t('actions'), + value: 'actions', + sortable: false, + }, + ]; }, async returnToProjects() { ++++++ semaphore.obsinfo ++++++ --- /var/tmp/diff_new_pack.hJA4Da/_old 2026-07-14 13:52:12.783689709 +0200 +++ /var/tmp/diff_new_pack.hJA4Da/_new 2026-07-14 13:52:12.787689847 +0200 @@ -1,5 +1,5 @@ name: semaphore -version: 2.18.25 -mtime: 1783598248 -commit: 7c3789c890d7e92fb8ca2a63ad74babf460a13fa +version: 2.18.26 +mtime: 1783856229 +commit: ee781a032ccb58c61de6b9a70993f9b6dc77dcf8 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/semaphore/vendor.tar.gz /work/SRC/openSUSE:Factory/.semaphore.new.1991/vendor.tar.gz differ: char 151, line 2 ++++++ web-2.18.25.tar.gz -> web-2.18.26.tar.gz ++++++ /work/SRC/openSUSE:Factory/semaphore/web-2.18.25.tar.gz /work/SRC/openSUSE:Factory/.semaphore.new.1991/web-2.18.26.tar.gz differ: char 31, line 1
