fix(规则脚本): 规则引擎 Groovy 脚本注入导致远程代码执行漏洞修复

This commit is contained in:
gx_ma
2026-10-01 03:05:41 +08:00
parent 284b714c1e
commit 890a338460
56 changed files with 2124 additions and 88 deletions

View File

@@ -0,0 +1,101 @@
<template>
<el-dialog
:title="$t('forceChangePwdTitle')"
:visible="visible"
width="460px"
append-to-body
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-alert :title="$t('forceChangePwdTip')" type="warning" :closable="false" show-icon style="margin-bottom: 16px" />
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item :label="$t('oldPassword')" prop="oldPassword">
<el-input v-model="form.oldPassword" :placeholder="$t('system.user.profile.oldPasswordPlaceholder')" type="password" show-password />
</el-form-item>
<el-form-item :label="$t('newPassword')" prop="newPassword">
<el-input v-model="form.newPassword" :placeholder="$t('system.user.profile.newPasswordPlaceholder')" type="password" show-password />
</el-form-item>
<el-form-item :label="$t('confirmPassword')" prop="confirmPassword">
<el-input v-model="form.confirmPassword" :placeholder="$t('system.user.profile.confirmPasswordPlaceholder')" type="password" show-password />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="loading" @click="submit">{{ $t('forceChangePwdConfirm') }}</el-button>
</div>
</el-dialog>
</template>
<script>
import { updateUserPwd } from '@/api/system/user';
export default {
name: 'ForceChangePwd',
data() {
const equalToPassword = (rule, value, callback) => {
if (this.form.newPassword !== value) {
callback(new Error(this.$t('system.user.profile.pwdNotMatch')));
} else {
callback();
}
};
return {
loading: false,
form: {
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined,
},
rules: {
oldPassword: [{ required: true, message: this.$t('system.user.profile.oldPasswordRequired'), trigger: 'blur' }],
newPassword: [
{ required: true, message: this.$t('system.user.profile.newPasswordRequired'), trigger: 'blur' },
{ min: 6, max: 20, message: this.$t('system.user.profile.passwordLength'), trigger: 'blur' },
{
trigger: 'blur',
validator: (rule, value, callback) => {
var passwordreg = /(?![A-Z]*$)(?![a-z]*$)(?![0-9]*$)(?![^a-zA-Z0-9]*$)/;
if (!passwordreg.test(value)) {
callback(new Error(this.$t('system.user.profile.passwordStrength')));
} else {
callback();
}
},
},
],
confirmPassword: [
{ required: true, message: this.$t('system.user.profile.confirmPasswordRequired'), trigger: 'blur' },
{ required: true, validator: equalToPassword, trigger: 'blur' },
],
},
};
},
computed: {
visible() {
return this.$store.state.user.needChangePwd;
},
},
methods: {
submit() {
this.$refs.form.validate((valid) => {
if (!valid) {
return;
}
this.loading = true;
updateUserPwd({ oldPassword: this.form.oldPassword, newPassword: this.form.newPassword })
.then(() => {
this.$modal.msgSuccess(this.$t('updateSuccessReLogin'));
// Stay on this isolated page until logout has completed.
return this.$store.dispatch('LogOut').then(() => {
location.href = '/login';
});
})
.catch(() => {})
.finally(() => {
this.loading = false;
});
});
},
},
};
</script>

View File

@@ -132,5 +132,8 @@
"batchCancelAuth": "Batch Cancel Authorization",
"cancelAuth": "Cancel Authorization",
"clean": "Clear",
"cleanSuccess": "Clear Successful"
"cleanSuccess": "Clear Successful",
"forceChangePwdTitle": "Change the default password",
"forceChangePwdTip": "This account is still using the factory default password, which is public and trivially guessable. To keep the platform and its devices safe, set a new password now. You will need to sign in again afterwards.",
"forceChangePwdConfirm": "Change password and sign in again"
}

View File

@@ -25,11 +25,24 @@ const i18n = new VueI18n({
locale.use(Element, {
i18n: (key, value) => i18n.t(key, value),
});
listLanguage({ pageNum: 1, pageSize: 1000 }).then((res) => {
res.rows.forEach((row) => {
if (langs[row.language]) langs[row.language].language = row.langName;
});
});
let languageNamesLoaded = false;
let languageNamesRequest;
function loadLanguageNames() {
if (languageNamesLoaded) return Promise.resolve();
if (!languageNamesRequest) {
languageNamesRequest = listLanguage({ pageNum: 1, pageSize: 1000 }).then((res) => {
res.rows.forEach((row) => {
if (langs[row.language]) langs[row.language].language = row.langName;
});
languageNamesLoaded = true;
}).catch(() => {
// Built-in translations remain available if optional display names cannot load.
}).finally(() => {
languageNamesRequest = undefined;
});
}
return languageNamesRequest;
}
function getJson(contexts) {
let json = {};
@@ -40,4 +53,4 @@ function getJson(contexts) {
}
// 导出langs 给切换语言的时候用
export { i18n, langs };
export { i18n, langs, loadLanguageNames };

View File

@@ -132,5 +132,8 @@
"batchCancelAuth": "批量取消授权",
"cancelAuth": "取消授权",
"clean": "清空",
"cleanSuccess": "清空成功"
"cleanSuccess": "清空成功",
"forceChangePwdTitle": "请修改初始密码",
"forceChangePwdTip": "检测到当前账号仍在使用出厂默认口令,该口令已公开且极易被爆破。为保障平台与设备安全,请立即设置新密码,修改后需要重新登录。",
"forceChangePwdConfirm": "确认修改并重新登录"
}

View File

@@ -5,6 +5,8 @@ import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { isRelogin } from '@/utils/request'
import { PASSWORD_CHANGE_ROUTE } from '@/utils/passwordChange'
import { loadLanguageNames } from '@/lang'
NProgress.configure({ showSpinner: false })
@@ -24,10 +26,18 @@ router.beforeEach((to, from, next) => {
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => {
isRelogin.show = false
// Resolve the server-owned password state before mounting any business page.
if (store.state.user.needChangePwd) {
if (to.path === PASSWORD_CHANGE_ROUTE) next()
else next({ path: PASSWORD_CHANGE_ROUTE, replace: true })
return
}
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
next(to.path === PASSWORD_CHANGE_ROUTE
? { path: '/index', replace: true }
: { ...to, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
@@ -35,6 +45,11 @@ router.beforeEach((to, from, next) => {
next({ path: '/' })
})
})
} else if (store.state.user.needChangePwd) {
if (to.path === PASSWORD_CHANGE_ROUTE) next()
else next({ path: PASSWORD_CHANGE_ROUTE, replace: true })
} else if (to.path === PASSWORD_CHANGE_ROUTE) {
next({ path: '/index', replace: true })
} else {
next()
}
@@ -53,4 +68,6 @@ router.beforeEach((to, from, next) => {
router.afterEach(() => {
NProgress.done()
// Avoid the eager language-list request before GetInfo has resolved the login state.
if (!getToken() || !store.state.user.needChangePwd) loadLanguageNames()
})

View File

@@ -53,6 +53,11 @@ export const constantRoutes = [
component: () => import('@/views/login'),
hidden: true,
},
{
path: '/change-password',
component: () => import('@/components/ForceChangePwd'),
hidden: true,
},
{
path: '/register',
component: () => import('@/views/register'),

View File

@@ -3,6 +3,8 @@ import { getToken, setToken, removeToken } from '@/utils/auth'
import { isHttp, isEmpty } from "@/utils/validate"
import defAva from '@/assets/images/profile.jpg'
const NEED_CHANGE_PWD_KEY = 'needChangePwd'
const user = {
state: {
token: getToken(),
@@ -10,7 +12,9 @@ const user = {
name: '',
avatar: '',
roles: [],
permissions: []
permissions: [],
// 出厂默认口令未修改标记,登录接口下发,持久化以覆盖刷新场景
needChangePwd: localStorage.getItem(NEED_CHANGE_PWD_KEY) === 'true'
},
mutations: {
@@ -31,6 +35,14 @@ const user = {
},
SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions
},
SET_NEED_CHANGE_PWD: (state, needChangePwd) => {
state.needChangePwd = needChangePwd
if (needChangePwd) {
localStorage.setItem(NEED_CHANGE_PWD_KEY, 'true')
} else {
localStorage.removeItem(NEED_CHANGE_PWD_KEY)
}
}
},
@@ -45,6 +57,7 @@ const user = {
login(username, password, code, uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
commit('SET_NEED_CHANGE_PWD', res.needChangePwd === true)
resolve()
}).catch(error => {
reject(error)
@@ -56,6 +69,7 @@ const user = {
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(res => {
commit('SET_NEED_CHANGE_PWD', res.needChangePwd === true)
const user = res.user
let avatar = user.avatar || ""
if (!isHttp(avatar)) {
@@ -84,6 +98,7 @@ const user = {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
commit('SET_NEED_CHANGE_PWD', false)
removeToken()
resolve()
}).catch(error => {
@@ -96,6 +111,7 @@ const user = {
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
commit('SET_NEED_CHANGE_PWD', false)
removeToken()
resolve()
})

View File

@@ -0,0 +1,14 @@
// UX coordination only. The backend remains authoritative for access control.
export const PASSWORD_CHANGE_ROUTE = '/change-password';
export function isPasswordChangeResponse(data, status) {
return !!data && data.needChangePwd === true && (status === 403 || data.code === 403);
}
export function allowedWhileChangingPassword(config) {
const method = (config.method || 'get').toUpperCase();
const path = (config.url || '').split(/[?#]/)[0];
return (method === 'GET' && ['/getInfo', '/getRouters', '/system/user/profile'].includes(path))
|| (method === 'PUT' && path === '/system/user/profile/updatePwd')
|| (method === 'POST' && path === '/logout');
}

View File

@@ -1,6 +1,8 @@
import axios from 'axios';
import { Notification, MessageBox, Message, Loading } from 'element-ui';
import store from '@/store';
import router from '@/router';
import { PASSWORD_CHANGE_ROUTE, allowedWhileChangingPassword, isPasswordChangeResponse } from '@/utils/passwordChange';
import { getToken } from '@/utils/auth';
import errorCode from '@/utils/errorCode';
import { tansParams, blobValidate } from '@/utils/ruoyi';
@@ -10,6 +12,20 @@ import { saveAs } from 'file-saver';
let downloadLoadingInstance;
// 是否显示重新登录
export let isRelogin = { show: false };
let passwordChangeNavigation;
function requirePasswordChange(message) {
if (getToken()) {
store.commit('SET_NEED_CHANGE_PWD', true);
if (!passwordChangeNavigation && router.currentRoute.path !== PASSWORD_CHANGE_ROUTE) {
passwordChangeNavigation = Promise.resolve(router.replace({ path: PASSWORD_CHANGE_ROUTE }))
.catch(() => {}).finally(() => { passwordChangeNavigation = undefined; });
}
}
const error = new axios.Cancel(message || '请先修改出厂默认密码');
error.needChangePwd = true;
return Promise.reject(error);
}
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';
// 创建axios实例
@@ -26,6 +42,10 @@ service.interceptors.request.use(
config.headers.language = store.state.settings.language;
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
if (getToken() && !isToken && store.state.user.needChangePwd && !allowedWhileChangingPassword(config)) {
// Stop background work locally; never disguise a rejected request as success.
return requirePasswordChange();
}
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false;
if (getToken() && !isToken) {
@@ -65,13 +85,14 @@ service.interceptors.request.use(
},
(error) => {
console.log(error);
Promise.reject(error);
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(res) => {
if (isPasswordChangeResponse(res.data, res.status)) return requirePasswordChange(res.data.msg);
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息,优先使用服务端消息
@@ -111,6 +132,11 @@ service.interceptors.response.use(
}
},
(error) => {
if (axios.isCancel(error)) return Promise.reject(error);
const response = error.response;
if (response && isPasswordChangeResponse(response.data, response.status)) {
return requirePasswordChange(response.data.msg);
}
console.log('err' + error);
let { message } = error;
if (message == 'Network Error') {
@@ -153,6 +179,10 @@ export function download(url, params, filename, config) {
downloadLoadingInstance.close();
})
.catch((r) => {
if (axios.isCancel(r)) {
downloadLoadingInstance.close();
return;
}
console.error(r);
Message.error('下载文件出现错误,请联系管理员!');
downloadLoadingInstance.close();