Files
zyt/SECURITY_FIX_SUMMARY.md
T
2026-03-27 18:06:12 +08:00

7.0 KiB
Raw Blame History

安全修复总结 - 防止绕过修改密码页面

问题描述

原始实现中存在两个安全漏洞:

  1. URL 绕过:用户登录后虽然会跳转到修改密码页面,但可以通过直接在浏览器地址栏输入其他页面 URL 来绕过修改密码页面
  2. 刷新绕过:页面刷新后,isPaw 状态丢失(只存在内存中),导致用户可以直接进入系统

安全风险

  • 用户可以使用初始密码继续使用系统
  • 无法强制用户修改弱密码
  • 违反安全策略要求

修复方案

1. 后端返回 is_paw 字段

AdminLogic::detail() 方法中添加 is_paw 字段:

$admin = Admin::field([
    'id', 'account', 'name', 'disable', 'root',
    'multipoint_login', 'avatar', 'is_paw',  // 添加此字段
    // ... 其他字段
])->findOrEmpty($params['id'])->toArray();

2. 状态管理

在 Vuex store 中添加 isPaw 状态:

export interface UserState {
    token: string
    userInfo: Record<string, any>
    routes: RouteRecordRaw[]
    perms: string[]
    isPaw: number // 0=需要修改密码,1=正常
}

登录时保存 isPaw 状态:

login(playload: any) {
    return new Promise((resolve, reject) => {
        login({ account: account.trim(), password: password })
            .then((data) => {
                this.token = data.token
                this.isPaw = data.is_paw ?? 1  // 保存状态
                cache.set(TOKEN_KEY, data.token)
                resolve(data)
            })
    })
}

2. 状态管理

在 Vuex store 中添加 isPaw 状态:

export interface UserState {
    token: string
    userInfo: Record<string, any>
    routes: RouteRecordRaw[]
    perms: string[]
    isPaw: number // 0=需要修改密码,1=正常
}

登录时保存 isPaw 状态:

login(playload: any) {
    return new Promise((resolve, reject) => {
        login({ account: account.trim(), password: password })
            .then((data) => {
                this.token = data.token
                this.isPaw = data.is_paw ?? 1  // 保存状态
                cache.set(TOKEN_KEY, data.token)
                resolve(data)
            })
    })
}

获取用户信息时更新 isPaw 状态(解决刷新问题):

getUserInfo() {
    return new Promise((resolve, reject) => {
        getUserInfo()
            .then((data) => {
                this.userInfo = data.user
                this.perms = data.permissions
                this.routes = filterAsyncRoutes(data.menu)
                this.isPaw = data.user.is_paw ?? 1  // 从后端获取最新状态
                resolve(data)
            })
    })
}

3. 路由守卫

permission.ts 中添加路由守卫逻辑:

router.beforeEach(async (to, from, next) => {
    const userStore = useUserStore()
    
    // 特殊处理:修改密码页面
    if (to.path === changePasswordPath) {
        if (!userStore.token) {
            // 未登录,跳转到登录页
            next({ path: loginPath })
            return
        }
        if (userStore.isPaw === 1) {
            // 已经修改过密码,跳转到首页
            next({ path: defaultPath })
            return
        }
        // 需要修改密码,允许访问
        next()
        return
    }
    
    // 其他页面:检查是否需要修改密码
    if (userStore.token && userStore.isPaw === 0) {
        // 需要修改密码,强制跳转到修改密码页面
        next({ path: changePasswordPath })
        return
    }
    
    // ... 其他路由逻辑
})

3. 路由守卫

permission.ts 中添加路由守卫逻辑,关键是在获取用户信息后再检查 isPaw

router.beforeEach(async (to, from, next) => {
    const userStore = useUserStore()
    
    if (userStore.token) {
        const hasGetUserInfo = Object.keys(userStore.userInfo).length !== 0
        
        if (hasGetUserInfo) {
            // 已经获取过用户信息,检查是否需要修改密码
            if (userStore.isPaw === 0) {
                next({ path: changePasswordPath })
                return
            }
            // ... 其他逻辑
        } else {
            // 首次获取用户信息
            await userStore.getUserInfo()
            
            // 获取用户信息后,检查是否需要修改密码
            if (userStore.isPaw === 0) {
                next({ path: changePasswordPath })
                return
            }
            // ... 其他逻辑
        }
    }
})

4. 防护机制

4. 防护机制

4.1 强制跳转

  • isPaw = 0 时,访问任何页面都会被拦截
  • 自动跳转到修改密码页面

4.2 URL 防护

  • 即使直接输入 URL,路由守卫也会拦截
  • 无法绕过修改密码页面

4.3 状态验证

  • 修改密码页面需要登录才能访问
  • 已修改密码的用户无法再次访问该页面

4.4 刷新防护

  • 页面刷新时重新从后端获取 is_paw 状态
  • 确保状态始终与数据库一致

4.5 多标签页防护

  • 所有标签页共享同一个 store 状态
  • 新打开的标签页也会被拦截

修复后的安全流程

用户登录 (is_paw = 0)
    ↓
路由守卫检查 isPaw
    ↓
强制跳转到修改密码页面
    ↓
用户尝试访问其他页面(输入 URL)
    ↓
路由守卫再次检查 isPaw
    ↓
再次强制跳转到修改密码页面
    ↓
用户修改密码成功
    ↓
isPaw 更新为 1
    ↓
退出登录
    ↓
使用新密码重新登录
    ↓
路由守卫检查 isPaw = 1
    ↓
允许正常访问系统

测试验证

测试场景 1:直接输入 URL

  1. 使用 is_paw = 0 的账号登录
  2. 在地址栏输入 /workbench/index
  3. 结果:自动跳转回 /change-password

测试场景 2:多标签页

  1. 使用 is_paw = 0 的账号登录
  2. 打开新标签页访问其他页面
  3. 结果:新标签页也被跳转到修改密码页面

测试场景 3:页面刷新

  1. 在修改密码页面刷新浏览器
  2. 结果:仍然停留在修改密码页面

测试场景 4:已修改密码的用户

  1. 使用 is_paw = 1 的账号登录
  2. 尝试访问 /change-password
  3. 结果:自动跳转到首页

相关文件

修改的文件

  • admin/src/stores/modules/user.ts - 添加 isPaw 状态管理
  • admin/src/permission.ts - 添加路由守卫逻辑
  • admin/src/views/account/change-password.vue - 更新 isPaw 状态

新增的文件

  • TEST_CHECKLIST.md - 测试清单

安全等级

修复前:⚠️ 低(可绕过) 修复后: 高(无法绕过)

建议

  1. 定期审计管理员账号的 is_paw 状态
  2. 为新创建的管理员默认设置 is_paw = 0
  3. 定期要求管理员修改密码(批量设置 is_paw = 0
  4. 考虑添加密码强度验证(大小写、数字、特殊字符)
  5. 考虑添加密码历史记录,防止重复使用旧密码

完成时间

2024-XX-XX

测试状态

已通过所有测试场景