fix: request a password to change sensitive user data (#5629)

This commit is contained in:
Ariel Leyva
2026-01-03 02:44:03 -05:00
committed by GitHub
parent 943e5340d0
commit b8151a038a
9 changed files with 103 additions and 26 deletions

View File

@@ -6,22 +6,23 @@ import (
) )
var ( var (
ErrEmptyKey = errors.New("empty key") ErrEmptyKey = errors.New("empty key")
ErrExist = errors.New("the resource already exists") ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist") ErrNotExist = errors.New("the resource does not exist")
ErrEmptyPassword = errors.New("password is empty") ErrEmptyPassword = errors.New("password is empty")
ErrEasyPassword = errors.New("password is too easy") ErrEasyPassword = errors.New("password is too easy")
ErrEmptyUsername = errors.New("username is empty") ErrEmptyUsername = errors.New("username is empty")
ErrEmptyRequest = errors.New("empty request") ErrEmptyRequest = errors.New("empty request")
ErrScopeIsRelative = errors.New("scope is a relative path") ErrScopeIsRelative = errors.New("scope is a relative path")
ErrInvalidDataType = errors.New("invalid data type") ErrInvalidDataType = errors.New("invalid data type")
ErrIsDirectory = errors.New("file is directory") ErrIsDirectory = errors.New("file is directory")
ErrInvalidOption = errors.New("invalid option") ErrInvalidOption = errors.New("invalid option")
ErrInvalidAuthMethod = errors.New("invalid auth method") ErrInvalidAuthMethod = errors.New("invalid auth method")
ErrPermissionDenied = errors.New("permission denied") ErrPermissionDenied = errors.New("permission denied")
ErrInvalidRequestParams = errors.New("invalid request params") ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent") ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted") ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
ErrCurrentPasswordIncorrect = errors.New("the current password is incorrect")
) )
type ErrShortPassword struct { type ErrShortPassword struct {

View File

@@ -8,12 +8,13 @@ export async function get(id: number) {
return fetchJSON<IUser>(`/api/users/${id}`, {}); return fetchJSON<IUser>(`/api/users/${id}`, {});
} }
export async function create(user: IUser) { export async function create(user: IUser, currentPassword: string) {
const res = await fetchURL(`/api/users`, { const res = await fetchURL(`/api/users`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
what: "user", what: "user",
which: [], which: [],
current_password: currentPassword,
data: user, data: user,
}), }),
}); });
@@ -25,12 +26,17 @@ export async function create(user: IUser) {
throw new StatusError(await res.text(), res.status); throw new StatusError(await res.text(), res.status);
} }
export async function update(user: Partial<IUser>, which = ["all"]) { export async function update(
user: Partial<IUser>,
which = ["all"],
currentPassword: string | null = null
) {
await fetchURL(`/api/users/${user.id}`, { await fetchURL(`/api/users/${user.id}`, {
method: "PUT", method: "PUT",
body: JSON.stringify({ body: JSON.stringify({
what: "user", what: "user",
which: which, which: which,
...(currentPassword != null ? { current_password: currentPassword } : {}),
data: user, data: user,
}), }),
}); });

View File

@@ -258,7 +258,8 @@
"userManagement": "User Management", "userManagement": "User Management",
"userUpdated": "User updated!", "userUpdated": "User updated!",
"username": "Username", "username": "Username",
"users": "Users" "users": "Users",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Help", "help": "Help",

View File

@@ -5,6 +5,7 @@ interface ISettings {
minimumPasswordLength: number; minimumPasswordLength: number;
userHomeBasePath: string; userHomeBasePath: string;
defaults: SettingsDefaults; defaults: SettingsDefaults;
authMethod: string;
rules: any[]; rules: any[];
branding: SettingsBranding; branding: SettingsBranding;
tus: SettingsTus; tus: SettingsTus;

View File

@@ -69,6 +69,15 @@
v-model="passwordConf" v-model="passwordConf"
name="passwordConf" name="passwordConf"
/> />
<input
v-if="isCurrentPasswordRequired"
:class="passwordClass"
type="password"
:placeholder="t('settings.currentPassword')"
v-model="currentPassword"
name="current_password"
autocomplete="current-password"
/>
</div> </div>
<div class="card-action"> <div class="card-action">
@@ -87,7 +96,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { users as api } from "@/api"; import { users as api, settings } from "@/api";
import AceEditorTheme from "@/components/settings/AceEditorTheme.vue"; import AceEditorTheme from "@/components/settings/AceEditorTheme.vue";
import Languages from "@/components/settings/Languages.vue"; import Languages from "@/components/settings/Languages.vue";
import { computed, inject, onMounted, ref } from "vue"; import { computed, inject, onMounted, ref } from "vue";
@@ -102,6 +111,8 @@ const $showError = inject<IToastError>("$showError")!;
const password = ref<string>(""); const password = ref<string>("");
const passwordConf = ref<string>(""); const passwordConf = ref<string>("");
const currentPassword = ref<string>("");
const isCurrentPasswordRequired = ref<boolean>(false);
const hideDotfiles = ref<boolean>(false); const hideDotfiles = ref<boolean>(false);
const singleClick = ref<boolean>(false); const singleClick = ref<boolean>(false);
const dateFormat = ref<boolean>(false); const dateFormat = ref<boolean>(false);
@@ -131,6 +142,9 @@ onMounted(async () => {
dateFormat.value = authStore.user.dateFormat; dateFormat.value = authStore.user.dateFormat;
aceEditorTheme.value = authStore.user.aceEditorTheme; aceEditorTheme.value = authStore.user.aceEditorTheme;
layoutStore.loading = false; layoutStore.loading = false;
const { authMethod } = await settings.get();
isCurrentPasswordRequired.value = authMethod == "json";
return true; return true;
}); });
@@ -140,6 +154,7 @@ const updatePassword = async (event: Event) => {
if ( if (
password.value !== passwordConf.value || password.value !== passwordConf.value ||
password.value === "" || password.value === "" ||
currentPassword.value === "" ||
authStore.user === null authStore.user === null
) { ) {
return; return;
@@ -151,7 +166,7 @@ const updatePassword = async (event: Event) => {
id: authStore.user.id, id: authStore.user.id,
password: password.value, password: password.value,
}; };
await api.update(data, ["password"]); await api.update(data, ["password"], currentPassword.value);
authStore.updateUser(data); authStore.updateUser(data);
$showSuccess(t("settings.passwordUpdated")); $showSuccess(t("settings.passwordUpdated"));
} catch (e: any) { } catch (e: any) {

View File

@@ -15,6 +15,19 @@
:isDefault="false" :isDefault="false"
:isNew="isNew" :isNew="isNew"
/> />
<p v-if="isCurrentPasswordRequired">
<label for="currentPassword">{{
t("settings.currentPassword")
}}</label>
<input
class="input input--block"
type="password"
v-model="currentPassword"
id="currentPassword"
autocomplete="current-password"
/>
</p>
</div> </div>
<div class="card-action"> <div class="card-action">
@@ -63,6 +76,8 @@ const error = ref<StatusError>();
const originalUser = ref<IUser>(); const originalUser = ref<IUser>();
const user = ref<IUser>(); const user = ref<IUser>();
const createUserDir = ref<boolean>(false); const createUserDir = ref<boolean>(false);
const currentPassword = ref<string>("");
const isCurrentPasswordRequired = ref<boolean>(false);
const $showError = inject<IToastError>("$showError")!; const $showError = inject<IToastError>("$showError")!;
const $showSuccess = inject<IToastSuccess>("$showSuccess")!; const $showSuccess = inject<IToastSuccess>("$showSuccess")!;
@@ -90,7 +105,12 @@ const fetchData = async () => {
try { try {
if (isNew.value) { if (isNew.value) {
const { defaults, createUserDir: _createUserDir } = await settings.get(); const {
authMethod,
defaults,
createUserDir: _createUserDir,
} = await settings.get();
isCurrentPasswordRequired.value = authMethod == "json";
createUserDir.value = _createUserDir; createUserDir.value = _createUserDir;
user.value = { user.value = {
...defaults, ...defaults,
@@ -101,6 +121,8 @@ const fetchData = async () => {
id: 0, id: 0,
}; };
} else { } else {
const { authMethod } = await settings.get();
isCurrentPasswordRequired.value = authMethod == "json";
const id = Array.isArray(route.params.id) const id = Array.isArray(route.params.id)
? route.params.id.join("") ? route.params.id.join("")
: route.params.id; : route.params.id;
@@ -151,11 +173,11 @@ const save = async (event: Event) => {
...user.value, ...user.value,
}; };
const loc = await api.create(newUser); const loc = await api.create(newUser, currentPassword.value);
router.push({ path: loc || "/settings/users" }); router.push({ path: loc || "/settings/users" });
$showSuccess(t("settings.userCreated")); $showSuccess(t("settings.userCreated"));
} else { } else {
await api.update(user.value); await api.update(user.value, ["all"], currentPassword.value);
if (user.value.id === authStore.user?.id) { if (user.value.id === authStore.user?.id) {
authStore.updateUser(user.value); authStore.updateUser(user.value);

View File

@@ -11,8 +11,9 @@ import (
) )
type modifyRequest struct { type modifyRequest struct {
What string `json:"what"` // Answer to: what data type? What string `json:"what"` // Answer to: what data type?
Which []string `json:"which"` // Answer to: which fields? Which []string `json:"which"` // Answer to: which fields?
CurrentPassword string `json:"current_password"` // Answer to: user logged password
} }
func NewHandler( func NewHandler(

View File

@@ -15,6 +15,7 @@ type settingsData struct {
MinimumPasswordLength uint `json:"minimumPasswordLength"` MinimumPasswordLength uint `json:"minimumPasswordLength"`
UserHomeBasePath string `json:"userHomeBasePath"` UserHomeBasePath string `json:"userHomeBasePath"`
Defaults settings.UserDefaults `json:"defaults"` Defaults settings.UserDefaults `json:"defaults"`
AuthMethod settings.AuthMethod `json:"authMethod"`
Rules []rules.Rule `json:"rules"` Rules []rules.Rule `json:"rules"`
Branding settings.Branding `json:"branding"` Branding settings.Branding `json:"branding"`
Tus settings.Tus `json:"tus"` Tus settings.Tus `json:"tus"`
@@ -30,6 +31,7 @@ var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
MinimumPasswordLength: d.settings.MinimumPasswordLength, MinimumPasswordLength: d.settings.MinimumPasswordLength,
UserHomeBasePath: d.settings.UserHomeBasePath, UserHomeBasePath: d.settings.UserHomeBasePath,
Defaults: d.settings.Defaults, Defaults: d.settings.Defaults,
AuthMethod: d.settings.AuthMethod,
Rules: d.settings.Rules, Rules: d.settings.Rules,
Branding: d.settings.Branding, Branding: d.settings.Branding,
Tus: d.settings.Tus, Tus: d.settings.Tus,

View File

@@ -12,6 +12,7 @@ import (
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/filebrowser/filebrowser/v2/auth"
fberrors "github.com/filebrowser/filebrowser/v2/errors" fberrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
) )
@@ -117,6 +118,12 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
if d.settings.AuthMethod == auth.MethodJSONAuth {
if !users.CheckPwd(req.CurrentPassword, d.user.Password) {
return http.StatusBadRequest, fberrors.ErrCurrentPasswordIncorrect
}
}
if len(req.Which) != 0 { if len(req.Which) != 0 {
return http.StatusBadRequest, nil return http.StatusBadRequest, nil
} }
@@ -153,6 +160,27 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
if d.settings.AuthMethod == auth.MethodJSONAuth {
var sensibleFields = map[string]struct{}{
"all": {},
"username": {},
"password": {},
"scope": {},
"lockPassword": {},
"commands": {},
"perm": {},
}
for _, field := range req.Which {
if _, ok := sensibleFields[field]; ok {
if !users.CheckPwd(req.CurrentPassword, d.user.Password) {
return http.StatusBadRequest, fberrors.ErrCurrentPasswordIncorrect
}
break
}
}
}
if req.Data.ID != d.raw.(uint) { if req.Data.ID != d.raw.(uint) {
return http.StatusBadRequest, nil return http.StatusBadRequest, nil
} }