Procházet zdrojové kódy

update 安全优化

ct před 13 hodinami
rodič
revize
1638b46230

+ 0 - 20
.env.prod-bjyjb

@@ -1,20 +0,0 @@
-# 页面标题
-VUE_APP_TITLE = 医生服务系统
-
-# 页面标题
-VUE_APP_IM_CONFIG = 医健宝医生端管理系统
-
-# 公司名称
-VUE_APP_COMPANY_NAME =医健宝智慧(北京)医药科技有限公司
-# ICP备案号
-VUE_APP_ICP_RECORD =京ICP备2025133930号-2
-# ICP网站访问地址
-VUE_APP_ICP_URL =https://beian.miit.gov.cn
-# 网站LOG
-VUE_APP_LOG_URL =@/assets/logo/bjyjb.jpg
-
-# 生产环境配置
-ENV = 'production'
-
-#互联网医院管理系统/生产环境
-VUE_APP_BASE_API = '/prod-api'

+ 0 - 24
.env.prod-hdt

@@ -1,24 +0,0 @@
-# 页面标题
-VUE_APP_TITLE = 医生服务系统
-
-# 腾讯IM
-VUE_APP_IM_CONFIG = 1600093112
-#openIM
-VUE_APP_API_ADDR=https://im.muyi88.com/api
-VUE_APP_WS_ADDR=wss://im.muyi88.com/msg_gateway
-IS_OPENIM =  false
-
-# 公司名称
-VUE_APP_COMPANY_NAME =河北红德堂医药连锁有限公司保定第五十七分公司
-# ICP备案号
-VUE_APP_ICP_RECORD =冀ICP备2025113252号
-# ICP网站访问地址
-VUE_APP_ICP_URL =https://beian.miit.gov.cn
-# 网站LOG
-VUE_APP_LOG_URL =@/assets/logo/hdt.jpg
-
-# 生产环境配置
-ENV = 'production'
-
-#互联网医院管理系统/生产环境
-VUE_APP_BASE_API = '/prod-api'

+ 0 - 24
.env.prod-jzzx

@@ -1,24 +0,0 @@
-# 页面标题
-VUE_APP_TITLE = 医生服务系统
-
-# 腾讯IM
-VUE_APP_IM_CONFIG = 1600092852
-#openIM
-VUE_APP_API_ADDR=https://im.muyi88.com/api
-VUE_APP_WS_ADDR=wss://im.muyi88.com/msg_gateway
-IS_OPENIM =  false
-
-# 公司名称
-VUE_APP_COMPANY_NAME =成都双流九州在线互联网医院有限公司
-# ICP备案号
-VUE_APP_ICP_RECORD =蜀ICP备2025130335号
-# ICP网站访问地址
-VUE_APP_ICP_URL =https://beian.miit.gov.cn
-# 网站LOG
-VUE_APP_LOG_URL =@/assets/logo/jzzx.png
-
-# 生产环境配置
-ENV = 'production'
-
-#互联网医院管理系统/生产环境
-VUE_APP_BASE_API = '/prod-api'

+ 0 - 23
.env.prod-myhk

@@ -1,23 +0,0 @@
-# 页面标题
-VUE_APP_TITLE = 医生服务系统
-
-# 腾讯IM
-VUE_APP_IM_CONFIG = 1600089394
-#openIM
-VUE_APP_API_ADDR=https://im.muyi88.com/api
-VUE_APP_WS_ADDR=wss://im.muyi88.com/msg_gateway
-VUE_APP_IS_OPENIM =  true
-# 公司名称
-VUE_APP_COMPANY_NAME =福州市木易华康医药有限公司
-# ICP备案号
-VUE_APP_ICP_RECORD =闽ICP备2020016609号-3
-# ICP网站访问地址
-VUE_APP_ICP_URL =https://beian.miit.gov.cn
-# 网站LOG
-VUE_APP_LOG_URL =@/assets/logo/myhk.png
-
-# 生产环境配置
-ENV = 'production'
-
-#互联网医院管理系统/生产环境
-VUE_APP_BASE_API = '/prod-api'

+ 0 - 20
.env.prod-ysy

@@ -1,20 +0,0 @@
-# 页面标题
-VUE_APP_TITLE = 医生服务系统
-
-# 页面标题
-VUE_APP_IM_CONFIG = 1600089873
-
-# 公司名称
-VUE_APP_COMPANY_NAME =益寿缘互联网医院有限公司
-# ICP备案号
-VUE_APP_ICP_RECORD =蜀ICP备2024052643号-6
-# ICP网站访问地址
-VUE_APP_ICP_URL =https://beian.miit.gov.cn
-# 网站LOG
-VUE_APP_LOG_URL =@/assets/logo/jnmy.png
-
-# 生产环境配置
-ENV = 'production'
-
-#互联网医院管理系统/生产环境
-VUE_APP_BASE_API = '/prod-api'

+ 1 - 0
package.json

@@ -47,6 +47,7 @@
     "clipboard": "2.0.6",
     "core-js": "3.8.1",
     "cos-js-sdk-v5": "^0.5.22",
+    "dompurify": "^3.4.13",
     "echarts": "^4.9.0",
     "element-ui": "2.15.5",
     "file-saver": "2.0.4",

+ 2 - 1
src/components/HeaderSearch/index.vue

@@ -22,6 +22,7 @@
 // make search results more in line with expectations
 import Fuse from 'fuse.js/dist/fuse.min.js'
 import path from 'path'
+import { safeOpen } from '@/utils/safeUrl'
 
 export default {
   name: 'HeaderSearch',
@@ -74,7 +75,7 @@ export default {
       if(this.ishttp(val.path)) {
         // http(s):// 路径新窗口打开
         const pindex = path.indexOf("http");
-        window.open(path.substr(pindex, path.length), "_blank");
+        safeOpen(path.substr(pindex, path.length), "_blank");
       } else {
         this.$router.push(val.path)
       }

+ 2 - 1
src/components/TopNav/index.vue

@@ -29,6 +29,7 @@
 
 <script>
 import { constantRoutes } from "@/router";
+import { safeOpen } from "@/utils/safeUrl";
 
 export default {
   data() {
@@ -137,7 +138,7 @@ export default {
       this.currentIndex = key;
       if (this.ishttp(key)) {
         // http(s):// 路径新窗口打开
-        window.open(key, "_blank");
+        safeOpen(key, "_blank");
       } else if (key.indexOf("/redirect") !== -1) {
         // /redirect 路径内部打开
         this.$router.push({ path: key.replace("/redirect", "") });

+ 2 - 0
src/directive/index.js

@@ -3,6 +3,7 @@ import hasPermi from './permission/hasPermi'
 import dialogDrag from './dialog/drag'
 import dialogDragWidth from './dialog/dragWidth'
 import dialogDragHeight from './dialog/dragHeight'
+import safeHtml from './safeHtml'
 
 const install = function(Vue) {
   Vue.directive('hasRole', hasRole)
@@ -10,6 +11,7 @@ const install = function(Vue) {
   Vue.directive('dialogDrag', dialogDrag)
   Vue.directive('dialogDragWidth', dialogDragWidth)
   Vue.directive('dialogDragHeight', dialogDragHeight)
+  Vue.directive('safe-html', safeHtml)
 }
 
 if (window.Vue) {

+ 16 - 0
src/directive/safeHtml.js

@@ -0,0 +1,16 @@
+import { sanitizeHtml } from '@/utils/sanitize'
+
+/**
+ * 替代 v-html:写入前经 DOMPurify 清洗
+ * 用法:v-safe-html="htmlString"
+ */
+export default {
+  bind(el, binding) {
+    el.innerHTML = sanitizeHtml(binding.value)
+  },
+  update(el, binding) {
+    if (binding.value !== binding.oldValue) {
+      el.innerHTML = sanitizeHtml(binding.value)
+    }
+  }
+}

+ 2 - 1
src/utils/im.js

@@ -3,7 +3,8 @@
 const config = {
   // 基础配置
   SDKAPPID: 0,
-  SECRET: 'openIM123',
+  // 禁止在前端硬编码 IM 密钥;由服务端签发 token
+  SECRET: '',
 
   // 服务器地址配置
   // API_ADDRESS: 'https://web.im.ysya.top/api', // OpenIM API地址

+ 9 - 19
src/utils/jsencrypt.js

@@ -1,30 +1,20 @@
 import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
 
-// 密钥对生成 http://web.chacuo.net/netrsakeypair
-
+// 仅保留公钥用于传输加密;私钥严禁下发到前端
 const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
   'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
 
-const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
-  '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
-  'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +
-  'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +
-  'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +
-  'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
-  'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
-  'UP8iWi1Qw0Y='
-
 // 加密
 export function encrypt(txt) {
   const encryptor = new JSEncrypt()
-  encryptor.setPublicKey(publicKey) // 设置公钥
-  return encryptor.encrypt(txt) // 对数据进行加密
+  encryptor.setPublicKey(publicKey)
+  return encryptor.encrypt(txt)
 }
 
-// 解密
-export function decrypt(txt) {
-  const encryptor = new JSEncrypt()
-  encryptor.setPrivateKey(privateKey) // 设置私钥
-  return encryptor.decrypt(txt) // 对数据进行解密
+/**
+ * 已移除前端私钥解密能力,防止「记住密码」Cookie 被还原明文。
+ * 历史加密 Cookie 将无法解密,需用户重新输入密码。
+ */
+export function decrypt() {
+  return ''
 }
-

+ 54 - 0
src/utils/safeUrl.js

@@ -0,0 +1,54 @@
+/**
+ * 安全打开外链:仅允许 http/https,并可按域名白名单限制
+ */
+const DEFAULT_HOST_WHITELIST = [
+  'cdwjyyh.com',
+  'jnmyunl.com',
+  'obs.jnmyunl.com',
+  'cos.his.cdwjyyh.com'
+]
+
+function isWhitelistedHost(hostname, whitelist) {
+  if (!hostname) return false
+  const host = hostname.toLowerCase()
+  return whitelist.some(domain => host === domain || host.endsWith('.' + domain))
+}
+
+/**
+ * @param {string} url
+ * @param {object} [options]
+ * @param {string[]} [options.hostWhitelist]
+ * @returns {boolean}
+ */
+export function isSafeHttpUrl(url, options = {}) {
+  if (!url || typeof url !== 'string') return false
+  const trimmed = url.trim()
+  try {
+    const u = new URL(trimmed)
+    if (u.protocol !== 'http:' && u.protocol !== 'https:') return false
+    if (u.username || u.password) return false
+    const whitelist = options.hostWhitelist || DEFAULT_HOST_WHITELIST
+    if (whitelist.length > 0 && !isWhitelistedHost(u.hostname, whitelist)) {
+      return false
+    }
+    return true
+  } catch (e) {
+    return false
+  }
+}
+
+/**
+ * 安全 window.open
+ */
+export function safeOpen(url, target = '_blank') {
+  if (!isSafeHttpUrl(url)) {
+    console.warn('拒绝打开不安全链接:', url)
+    return null
+  }
+  return window.open(url, target, 'noopener,noreferrer')
+}
+
+export default {
+  isSafeHttpUrl,
+  safeOpen
+}

+ 15 - 0
src/utils/sanitize.js

@@ -0,0 +1,15 @@
+import DOMPurify from 'dompurify'
+
+/**
+ * 清洗 HTML,防止存储型/反射型 XSS(配合 v-safe-html 使用)
+ */
+export function sanitizeHtml(html) {
+  if (html == null || html === '') {
+    return ''
+  }
+  return DOMPurify.sanitize(String(html), {
+    USE_PROFILES: { html: true },
+    FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'base'],
+    FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur']
+  })
+}

+ 1 - 1
src/views/components/doctorArticle/doctorArticleDetails.vue

@@ -44,7 +44,7 @@
                 <span v-if="item!=null">{{item.updateTime}}</span>
             </el-descriptions-item>
             <el-descriptions-item label="内容" >
-                <span  v-if="item!=null" v-html="item.content"></span>
+                <span  v-if="item!=null" v-safe-html="item.content"></span>
             </el-descriptions-item>
 
 

+ 4 - 4
src/views/components/msg/followMsgDetails.vue

@@ -237,10 +237,10 @@
         <el-descriptions-item label="咨询总结"><span v-if="drugReportItem!=null">{{drugReportItem.reportSummary}}</span></el-descriptions-item>
         <el-descriptions-item label="生活建议"span="3"><span v-if="drugReportItem!=null">{{drugReportItem.reportAdvice}}</span></el-descriptions-item>
         <el-descriptions-item label="备注"span="3"><span v-if="drugReportItem!=null">{{drugReportItem.remark}}</span></el-descriptions-item>
-        <el-descriptions-item label="评分"span="3"><span v-if="drugReportItem!=null"><span v-html="drugReportItem.pingStar"></span></span></el-descriptions-item>
-        <el-descriptions-item label="评价" span="3"><span v-if="drugReportItem!=null"> <span v-html="drugReportItem.pingContent"></span></span></el-descriptions-item>
-        <el-descriptions-item label="评价时间" span="3"><span v-if="drugReportItem!=null"> <span v-html="drugReportItem.pingTime"></span></span></el-descriptions-item>
-        <el-descriptions-item label="创建时间" span="3"><span v-if="drugReportItem!=null"> <span v-html="drugReportItem.createTime"></span></span></el-descriptions-item>
+        <el-descriptions-item label="评分"span="3"><span v-if="drugReportItem!=null"><span v-safe-html="drugReportItem.pingStar"></span></span></el-descriptions-item>
+        <el-descriptions-item label="评价" span="3"><span v-if="drugReportItem!=null"> <span v-safe-html="drugReportItem.pingContent"></span></span></el-descriptions-item>
+        <el-descriptions-item label="评价时间" span="3"><span v-if="drugReportItem!=null"> <span v-safe-html="drugReportItem.pingTime"></span></span></el-descriptions-item>
+        <el-descriptions-item label="创建时间" span="3"><span v-if="drugReportItem!=null"> <span v-safe-html="drugReportItem.createTime"></span></span></el-descriptions-item>
       </el-descriptions>
       <span slot="footer" class="dialog-footer">
         <el-button @click="drugReportDialogVisible = false">取 消</el-button>

+ 4 - 4
src/views/components/msg/msgDetails.vue

@@ -237,10 +237,10 @@
         <el-descriptions-item label="咨询总结"><span v-if="drugReportItem!=null">{{drugReportItem.reportSummary}}</span></el-descriptions-item>
         <el-descriptions-item label="生活建议"span="3"><span v-if="drugReportItem!=null">{{drugReportItem.reportAdvice}}</span></el-descriptions-item>
         <el-descriptions-item label="备注"span="3"><span v-if="drugReportItem!=null">{{drugReportItem.remark}}</span></el-descriptions-item>
-        <el-descriptions-item label="评分"span="3"><span v-if="drugReportItem!=null"><span v-html="drugReportItem.pingStar"></span></span></el-descriptions-item>
-        <el-descriptions-item label="评价" span="3"><span v-if="drugReportItem!=null"> <span v-html="drugReportItem.pingContent"></span></span></el-descriptions-item>
-        <el-descriptions-item label="评价时间" span="3"><span v-if="drugReportItem!=null"> <span v-html="drugReportItem.pingTime"></span></span></el-descriptions-item>
-        <el-descriptions-item label="创建时间" span="3"><span v-if="drugReportItem!=null"> <span v-html="drugReportItem.createTime"></span></span></el-descriptions-item>
+        <el-descriptions-item label="评分"span="3"><span v-if="drugReportItem!=null"><span v-safe-html="drugReportItem.pingStar"></span></span></el-descriptions-item>
+        <el-descriptions-item label="评价" span="3"><span v-if="drugReportItem!=null"> <span v-safe-html="drugReportItem.pingContent"></span></span></el-descriptions-item>
+        <el-descriptions-item label="评价时间" span="3"><span v-if="drugReportItem!=null"> <span v-safe-html="drugReportItem.pingTime"></span></span></el-descriptions-item>
+        <el-descriptions-item label="创建时间" span="3"><span v-if="drugReportItem!=null"> <span v-safe-html="drugReportItem.createTime"></span></span></el-descriptions-item>
       </el-descriptions>
       <span slot="footer" class="dialog-footer">
         <el-button @click="drugReportDialogVisible = false">取 消</el-button>

+ 4 - 3
src/views/login.vue

@@ -124,13 +124,14 @@ export default {
     },
     getCookie() {
       const account = Cookies.get("account");
-      const password = Cookies.get("password");
       const rememberMe = Cookies.get('rememberMe');
+      // 安全修复:不再从 Cookie 恢复密码
+      Cookies.remove("password");
       // 使用扩展运算符保留原来 loginForm 中的属性,包括 type
       this.loginForm = {
         ...this.loginForm,
         account: account === undefined ? this.loginForm.account : account,
-        password: password === undefined ? this.loginForm.password : decrypt(password),
+        password: "",
         rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
       };
     },
@@ -140,7 +141,7 @@ export default {
           this.loading = true;
           if (this.loginForm.rememberMe) {
             Cookies.set("account", this.loginForm.account, { expires: 30 });
-            Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
+            Cookies.remove("password");
             Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
           } else {
             Cookies.remove("account");