diff --git a/gateway/html/dashboard.html b/gateway/html/dashboard.html index 9606890..1d2e0c4 100644 --- a/gateway/html/dashboard.html +++ b/gateway/html/dashboard.html @@ -773,10 +773,6 @@ ssoCookie.set('sso_user', JSON.stringify(data.user), 7); if (data.refresh_token) ssoCookie.set('sso_refresh_token', data.refresh_token, 30); - localStorage.setItem('sso_token', data.access_token); - localStorage.setItem('sso_user', JSON.stringify(data.user)); - if (data.refresh_token) localStorage.setItem('sso_refresh_token', data.refresh_token); - var redirect = new URLSearchParams(location.search).get('redirect'); if (redirect && isSafeRedirect(redirect)) { window.location.href = redirect; diff --git a/gateway/html/shared/nav-header.js b/gateway/html/shared/nav-header.js index 4126af2..6e9a4fb 100644 --- a/gateway/html/shared/nav-header.js +++ b/gateway/html/shared/nav-header.js @@ -31,11 +31,11 @@ */ window.SSOAuth = { getToken: function() { - return cookieGet('sso_token') || localStorage.getItem('sso_token'); + return cookieGet('sso_token'); }, getUser: function() { - var raw = cookieGet('sso_user') || localStorage.getItem('sso_user'); + var raw = cookieGet('sso_user'); try { return JSON.parse(raw); } catch(e) { return null; } }, diff --git a/gateway/nginx.conf b/gateway/nginx.conf index c365922..ff8fe4e 100644 --- a/gateway/nginx.conf +++ b/gateway/nginx.conf @@ -4,6 +4,10 @@ server { root /usr/share/nginx/html; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + # 대시보드 (로그인 포함) location = /dashboard { add_header Cache-Control "no-store, no-cache, must-revalidate"; diff --git a/sso-auth-service/controllers/authController.js b/sso-auth-service/controllers/authController.js index 243387f..e8be7d0 100644 --- a/sso-auth-service/controllers/authController.js +++ b/sso-auth-service/controllers/authController.js @@ -127,8 +127,17 @@ async function loginForm(req, res, next) { return res.status(400).json({ detail: 'Missing username or password' }); } + // Rate limiting (동일 로직: /login과 공유) + const attemptKey = `login_attempts:${username}`; + const attempts = parseInt(await redis.get(attemptKey)) || 0; + if (attempts >= MAX_LOGIN_ATTEMPTS) { + return res.status(429).json({ detail: '로그인 시도 횟수를 초과했습니다. 5분 후 다시 시도하세요' }); + } + const user = await userModel.findByUsername(username); if (!user) { + await redis.incr(attemptKey); + await redis.expire(attemptKey, LOGIN_LOCKOUT_SECONDS); return res.status(401).json({ detail: 'Incorrect username or password' }); } @@ -139,9 +148,14 @@ async function loginForm(req, res, next) { const valid = await userModel.verifyPassword(password, user.password_hash); if (!valid) { + await redis.incr(attemptKey); + await redis.expire(attemptKey, LOGIN_LOCKOUT_SECONDS); return res.status(401).json({ detail: 'Incorrect username or password' }); } + // 로그인 성공 시 시도 횟수 초기화 + await redis.del(attemptKey); + await userModel.updateLastLogin(user.user_id); const payload = createTokenPayload(user); diff --git a/system1-factory/api/config/security.js b/system1-factory/api/config/security.js index e58037e..4c27453 100644 --- a/system1-factory/api/config/security.js +++ b/system1-factory/api/config/security.js @@ -19,7 +19,7 @@ const helmetOptions = { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], - scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], // 개발 중 unsafe-eval 허용 + scriptSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:", "blob:"], fontSrc: ["'self'", "https://fonts.gstatic.com"], connectSrc: ["'self'", "https://api.technicalkorea.com"], diff --git a/system1-factory/web/index.html b/system1-factory/web/index.html index af33734..c631df7 100644 --- a/system1-factory/web/index.html +++ b/system1-factory/web/index.html @@ -10,7 +10,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then(function(r){r.forEach(function(reg){reg.unregister()});})} if('caches' in window){caches.keys().then(function(k){k.forEach(function(key){caches.delete(key)})})} - + - + - + diff --git a/system1-factory/web/pages/admin/equipment-detail.html b/system1-factory/web/pages/admin/equipment-detail.html index 0534502..e7a0b5b 100644 --- a/system1-factory/web/pages/admin/equipment-detail.html +++ b/system1-factory/web/pages/admin/equipment-detail.html @@ -315,7 +315,7 @@ - + - + - + diff --git a/system1-factory/web/pages/admin/notifications.html b/system1-factory/web/pages/admin/notifications.html index 708ee30..a450aa6 100644 --- a/system1-factory/web/pages/admin/notifications.html +++ b/system1-factory/web/pages/admin/notifications.html @@ -388,7 +388,7 @@ - + - + - + - + - + diff --git a/system1-factory/web/pages/attendance/annual-overview.html b/system1-factory/web/pages/attendance/annual-overview.html index cd1e8a4..2e47341 100644 --- a/system1-factory/web/pages/attendance/annual-overview.html +++ b/system1-factory/web/pages/attendance/annual-overview.html @@ -329,7 +329,7 @@ - + - + - + - + - + - + diff --git a/system1-factory/web/pages/attendance/vacation-approval.html b/system1-factory/web/pages/attendance/vacation-approval.html index 5175712..91d9ffb 100644 --- a/system1-factory/web/pages/attendance/vacation-approval.html +++ b/system1-factory/web/pages/attendance/vacation-approval.html @@ -124,7 +124,7 @@ - + - + - + - + - + - + diff --git a/system1-factory/web/pages/inspection/daily-patrol.html b/system1-factory/web/pages/inspection/daily-patrol.html index 4a9129a..12b5d80 100644 --- a/system1-factory/web/pages/inspection/daily-patrol.html +++ b/system1-factory/web/pages/inspection/daily-patrol.html @@ -210,7 +210,7 @@ })(); - + diff --git a/system1-factory/web/pages/inspection/zone-detail.html b/system1-factory/web/pages/inspection/zone-detail.html index 7a8894b..27fdc3a 100644 --- a/system1-factory/web/pages/inspection/zone-detail.html +++ b/system1-factory/web/pages/inspection/zone-detail.html @@ -305,7 +305,7 @@ })(); - + diff --git a/system1-factory/web/pages/profile/info.html b/system1-factory/web/pages/profile/info.html index a6092e9..6f7f8b5 100644 --- a/system1-factory/web/pages/profile/info.html +++ b/system1-factory/web/pages/profile/info.html @@ -321,7 +321,7 @@ - + diff --git a/system1-factory/web/pages/profile/password.html b/system1-factory/web/pages/profile/password.html index 612153d..3f337fb 100644 --- a/system1-factory/web/pages/profile/password.html +++ b/system1-factory/web/pages/profile/password.html @@ -391,7 +391,7 @@ - + diff --git a/system1-factory/web/pages/work/analysis.html b/system1-factory/web/pages/work/analysis.html index 62618a9..9e6856c 100644 --- a/system1-factory/web/pages/work/analysis.html +++ b/system1-factory/web/pages/work/analysis.html @@ -278,7 +278,7 @@ - + - + diff --git a/system1-factory/web/pages/work/report-create.html b/system1-factory/web/pages/work/report-create.html index 5c69ede..3b7230e 100644 --- a/system1-factory/web/pages/work/report-create.html +++ b/system1-factory/web/pages/work/report-create.html @@ -150,7 +150,7 @@ - + diff --git a/system1-factory/web/pages/work/tbm-create.html b/system1-factory/web/pages/work/tbm-create.html index 7c6cd4f..d54c333 100644 --- a/system1-factory/web/pages/work/tbm-create.html +++ b/system1-factory/web/pages/work/tbm-create.html @@ -844,7 +844,7 @@ - + diff --git a/system1-factory/web/pages/work/tbm-mobile.html b/system1-factory/web/pages/work/tbm-mobile.html index 863aebf..c44953c 100644 --- a/system1-factory/web/pages/work/tbm-mobile.html +++ b/system1-factory/web/pages/work/tbm-mobile.html @@ -297,7 +297,7 @@ - + diff --git a/system1-factory/web/pages/work/tbm.html b/system1-factory/web/pages/work/tbm.html index 44dec6c..e67acc6 100644 --- a/system1-factory/web/pages/work/tbm.html +++ b/system1-factory/web/pages/work/tbm.html @@ -561,7 +561,7 @@
- + diff --git a/system2-report/api/index.js b/system2-report/api/index.js index 0b95999..b404a71 100644 --- a/system2-report/api/index.js +++ b/system2-report/api/index.js @@ -77,7 +77,7 @@ app.use((err, req, res, next) => { logger.error('API Error:', { error: err.message, path: req.path }); res.status(statusCode).json({ success: false, - error: err.message || 'Internal Server Error' + error: '서버 오류가 발생했습니다' }); }); diff --git a/system2-report/web/js/api-base.js b/system2-report/web/js/api-base.js index b256703..ede779f 100644 --- a/system2-report/web/js/api-base.js +++ b/system2-report/web/js/api-base.js @@ -38,11 +38,11 @@ if ('serviceWorker' in navigator) { * SSO 토큰 가져오기 (쿠키 우선, localStorage 폴백) */ window.getSSOToken = function() { - return cookieGet('sso_token') || localStorage.getItem('sso_token'); + return cookieGet('sso_token'); }; window.getSSOUser = function() { - var raw = cookieGet('sso_user') || localStorage.getItem('sso_user'); + var raw = cookieGet('sso_user'); try { return raw ? JSON.parse(raw) : null; } catch(e) { return null; } }; diff --git a/system2-report/web/js/issue-detail.js b/system2-report/web/js/issue-detail.js index fb57af3..9a55957 100644 --- a/system2-report/web/js/issue-detail.js +++ b/system2-report/web/js/issue-detail.js @@ -232,11 +232,15 @@ function renderPhotos(d) { gallery.innerHTML = photos.map(photo => { const fullUrl = photo.startsWith('http') ? photo : `${baseUrl}${photo}`; return ` -
- 첨부 사진 +
+ 첨부 사진
`; }).join(''); + + gallery.querySelectorAll('.photo-item[data-url]').forEach(el => { + el.addEventListener('click', () => openPhotoModal(el.dataset.url)); + }); } /** diff --git a/system2-report/web/nginx.conf b/system2-report/web/nginx.conf index f621094..dbff11e 100644 --- a/system2-report/web/nginx.conf +++ b/system2-report/web/nginx.conf @@ -5,6 +5,10 @@ server { root /usr/share/nginx/html; index pages/safety/issue-report.html; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + # 사진 업로드를 위한 body 크기 제한 (base64 인코딩 시 원본 대비 ~33% 증가) client_max_body_size 50m; diff --git a/system2-report/web/pages/safety/issue-detail.html b/system2-report/web/pages/safety/issue-detail.html index 2a73603..9eb8bcb 100644 --- a/system2-report/web/pages/safety/issue-detail.html +++ b/system2-report/web/pages/safety/issue-detail.html @@ -472,6 +472,6 @@
- + diff --git a/system3-nonconformance/api/main.py b/system3-nonconformance/api/main.py index 7aa01ba..20d1957 100644 --- a/system3-nonconformance/api/main.py +++ b/system3-nonconformance/api/main.py @@ -73,9 +73,11 @@ async def health_check(): # 전역 예외 처리 @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): + import traceback + traceback.print_exc() return JSONResponse( status_code=500, - content={"detail": f"Internal server error: {str(exc)}"} + content={"detail": "서버 오류가 발생했습니다"} ) if __name__ == "__main__": diff --git a/system3-nonconformance/api/services/file_service.py b/system3-nonconformance/api/services/file_service.py index a10d134..9c5aca0 100644 --- a/system3-nonconformance/api/services/file_service.py +++ b/system3-nonconformance/api/services/file_service.py @@ -129,7 +129,9 @@ def delete_file(filepath: str): try: if filepath and filepath.startswith("/uploads/"): filename = filepath.replace("/uploads/", "") - full_path = os.path.join(UPLOAD_DIR, filename) + full_path = os.path.normpath(os.path.join(UPLOAD_DIR, filename)) + if not full_path.startswith(os.path.normpath(UPLOAD_DIR)): + raise ValueError("잘못된 파일 경로") if os.path.exists(full_path): os.remove(full_path) except Exception as e: diff --git a/system3-nonconformance/web/ai-assistant.html b/system3-nonconformance/web/ai-assistant.html index 387ca2d..d100811 100644 --- a/system3-nonconformance/web/ai-assistant.html +++ b/system3-nonconformance/web/ai-assistant.html @@ -10,6 +10,7 @@ + @@ -273,13 +274,13 @@ - + - + diff --git a/system3-nonconformance/web/issue-view.html b/system3-nonconformance/web/issue-view.html index 726747c..d807c73 100644 --- a/system3-nonconformance/web/issue-view.html +++ b/system3-nonconformance/web/issue-view.html @@ -108,7 +108,7 @@ - + diff --git a/system3-nonconformance/web/issues-archive.html b/system3-nonconformance/web/issues-archive.html index daf6c5f..7b26c87 100644 --- a/system3-nonconformance/web/issues-archive.html +++ b/system3-nonconformance/web/issues-archive.html @@ -198,7 +198,7 @@ - + diff --git a/system3-nonconformance/web/issues-dashboard.html b/system3-nonconformance/web/issues-dashboard.html index 6019494..e30c118 100644 --- a/system3-nonconformance/web/issues-dashboard.html +++ b/system3-nonconformance/web/issues-dashboard.html @@ -551,7 +551,7 @@ - + diff --git a/system3-nonconformance/web/issues-inbox.html b/system3-nonconformance/web/issues-inbox.html index 31de0ea..16979c4 100644 --- a/system3-nonconformance/web/issues-inbox.html +++ b/system3-nonconformance/web/issues-inbox.html @@ -370,7 +370,7 @@ - + diff --git a/system3-nonconformance/web/issues-management.html b/system3-nonconformance/web/issues-management.html index e3e0f8d..4f48943 100644 --- a/system3-nonconformance/web/issues-management.html +++ b/system3-nonconformance/web/issues-management.html @@ -20,6 +20,7 @@ + @@ -339,7 +340,7 @@ - + @@ -347,6 +348,6 @@ - + diff --git a/system3-nonconformance/web/nginx.conf b/system3-nonconformance/web/nginx.conf index 6c84fa9..dd57e19 100644 --- a/system3-nonconformance/web/nginx.conf +++ b/system3-nonconformance/web/nginx.conf @@ -4,6 +4,10 @@ server { client_max_body_size 10M; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + root /usr/share/nginx/html; index issues-dashboard.html; diff --git a/system3-nonconformance/web/reports-daily.html b/system3-nonconformance/web/reports-daily.html index 4c11236..123739a 100644 --- a/system3-nonconformance/web/reports-daily.html +++ b/system3-nonconformance/web/reports-daily.html @@ -183,7 +183,7 @@ - + diff --git a/system3-nonconformance/web/reports-monthly.html b/system3-nonconformance/web/reports-monthly.html index 6d61c20..cc37cdb 100644 --- a/system3-nonconformance/web/reports-monthly.html +++ b/system3-nonconformance/web/reports-monthly.html @@ -70,7 +70,7 @@ - + diff --git a/system3-nonconformance/web/reports-weekly.html b/system3-nonconformance/web/reports-weekly.html index 33e74c4..03ca831 100644 --- a/system3-nonconformance/web/reports-weekly.html +++ b/system3-nonconformance/web/reports-weekly.html @@ -69,7 +69,7 @@ - + diff --git a/system3-nonconformance/web/reports.html b/system3-nonconformance/web/reports.html index bd46686..650ac88 100644 --- a/system3-nonconformance/web/reports.html +++ b/system3-nonconformance/web/reports.html @@ -171,7 +171,7 @@ - + diff --git a/system3-nonconformance/web/static/js/components/common-header.js b/system3-nonconformance/web/static/js/components/common-header.js index cc28491..5ad4b51 100644 --- a/system3-nonconformance/web/static/js/components/common-header.js +++ b/system3-nonconformance/web/static/js/components/common-header.js @@ -641,7 +641,8 @@ class CommonHeader { }`; const icon = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'; - toast.innerHTML = `${message}`; + const _esc = s => { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }; + toast.innerHTML = `${_esc(message)}`; document.body.appendChild(toast); diff --git a/system3-nonconformance/web/static/js/core/auth-manager.js b/system3-nonconformance/web/static/js/core/auth-manager.js index a1db25a..c233fa8 100644 --- a/system3-nonconformance/web/static/js/core/auth-manager.js +++ b/system3-nonconformance/web/static/js/core/auth-manager.js @@ -72,14 +72,14 @@ class AuthManager { * SSO 토큰 가져오기 (쿠키 우선, localStorage 폴백) */ _getToken() { - return this._cookieGet('sso_token') || localStorage.getItem('sso_token'); + return this._cookieGet('sso_token'); } /** * SSO 사용자 정보 가져오기 (쿠키 우선, localStorage 폴백) */ _getUser() { - const ssoUser = this._cookieGet('sso_user') || localStorage.getItem('sso_user'); + const ssoUser = this._cookieGet('sso_user'); if (ssoUser && ssoUser !== 'undefined' && ssoUser !== 'null') { try { return JSON.parse(ssoUser); } catch(e) {} } diff --git a/system3-nonconformance/web/static/js/lib/purify.min.js b/system3-nonconformance/web/static/js/lib/purify.min.js new file mode 100644 index 0000000..b472a86 --- /dev/null +++ b/system3-nonconformance/web/static/js/lib/purify.min.js @@ -0,0 +1,3 @@ +/*! @license DOMPurify 3.2.4 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.4/LICENSE */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=R(Array.prototype.forEach),m=R(Array.prototype.lastIndexOf),p=R(Array.prototype.pop),f=R(Array.prototype.push),d=R(Array.prototype.splice),h=R(String.prototype.toLowerCase),g=R(String.prototype.toString),T=R(String.prototype.match),y=R(String.prototype.replace),E=R(String.prototype.indexOf),A=R(String.prototype.trim),_=R(Object.prototype.hasOwnProperty),S=R(RegExp.prototype.test),b=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:h;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function O(e){for(let t=0;t/gm),G=a(/\$\{[\w\W]*/gm),Y=a(/^data-[\-\w.\u00B7-\uFFFF]+$/),j=a(/^aria-[\-\w]+$/),X=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),q=a(/^(?:\w+script|data):/i),$=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),K=a(/^html$/i),V=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var Z=Object.freeze({__proto__:null,ARIA_ATTR:j,ATTR_WHITESPACE:$,CUSTOM_ELEMENT:V,DATA_ATTR:Y,DOCTYPE_NAME:K,ERB_EXPR:W,IS_ALLOWED_URI:X,IS_SCRIPT_OR_DATA:q,MUSTACHE_EXPR:B,TMPLIT_EXPR:G});const J=1,Q=3,ee=7,te=8,ne=9,oe=function(){return"undefined"==typeof window?null:window};var re=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:oe();const o=e=>t(e);if(o.version="3.2.4",o.removed=[],!n||!n.document||n.document.nodeType!==ne||!n.Element)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:R,Element:O,NodeFilter:B,NamedNodeMap:W=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:G,DOMParser:Y,trustedTypes:j}=n,q=O.prototype,$=v(q,"cloneNode"),V=v(q,"remove"),re=v(q,"nextSibling"),ie=v(q,"childNodes"),ae=v(q,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let le,ce="";const{implementation:se,createNodeIterator:ue,createDocumentFragment:me,getElementsByTagName:pe}=r,{importNode:fe}=a;let de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};o.isSupported="function"==typeof e&&"function"==typeof ae&&se&&void 0!==se.createHTMLDocument;const{MUSTACHE_EXPR:he,ERB_EXPR:ge,TMPLIT_EXPR:Te,DATA_ATTR:ye,ARIA_ATTR:Ee,IS_SCRIPT_OR_DATA:Ae,ATTR_WHITESPACE:_e,CUSTOM_ELEMENT:Se}=Z;let{IS_ALLOWED_URI:be}=Z,Ne=null;const Re=w({},[...L,...C,...x,...k,...U]);let we=null;const Oe=w({},[...z,...P,...H,...F]);let De=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ve=null,Le=null,Ce=!0,xe=!0,Me=!1,ke=!0,Ie=!1,Ue=!0,ze=!1,Pe=!1,He=!1,Fe=!1,Be=!1,We=!1,Ge=!0,Ye=!1,je=!0,Xe=!1,qe={},$e=null;const Ke=w({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ve=null;const Ze=w({},["audio","video","img","source","image","track"]);let Je=null;const Qe=w({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),et="http://www.w3.org/1998/Math/MathML",tt="http://www.w3.org/2000/svg",nt="http://www.w3.org/1999/xhtml";let ot=nt,rt=!1,it=null;const at=w({},[et,tt,nt],g);let lt=w({},["mi","mo","mn","ms","mtext"]),ct=w({},["annotation-xml"]);const st=w({},["title","style","font","a","script"]);let ut=null;const mt=["application/xhtml+xml","text/html"];let pt=null,ft=null;const dt=r.createElement("form"),ht=function(e){return e instanceof RegExp||e instanceof Function},gt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ft||ft!==e){if(e&&"object"==typeof e||(e={}),e=D(e),ut=-1===mt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,pt="application/xhtml+xml"===ut?g:h,Ne=_(e,"ALLOWED_TAGS")?w({},e.ALLOWED_TAGS,pt):Re,we=_(e,"ALLOWED_ATTR")?w({},e.ALLOWED_ATTR,pt):Oe,it=_(e,"ALLOWED_NAMESPACES")?w({},e.ALLOWED_NAMESPACES,g):at,Je=_(e,"ADD_URI_SAFE_ATTR")?w(D(Qe),e.ADD_URI_SAFE_ATTR,pt):Qe,Ve=_(e,"ADD_DATA_URI_TAGS")?w(D(Ze),e.ADD_DATA_URI_TAGS,pt):Ze,$e=_(e,"FORBID_CONTENTS")?w({},e.FORBID_CONTENTS,pt):Ke,ve=_(e,"FORBID_TAGS")?w({},e.FORBID_TAGS,pt):{},Le=_(e,"FORBID_ATTR")?w({},e.FORBID_ATTR,pt):{},qe=!!_(e,"USE_PROFILES")&&e.USE_PROFILES,Ce=!1!==e.ALLOW_ARIA_ATTR,xe=!1!==e.ALLOW_DATA_ATTR,Me=e.ALLOW_UNKNOWN_PROTOCOLS||!1,ke=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,Ie=e.SAFE_FOR_TEMPLATES||!1,Ue=!1!==e.SAFE_FOR_XML,ze=e.WHOLE_DOCUMENT||!1,Fe=e.RETURN_DOM||!1,Be=e.RETURN_DOM_FRAGMENT||!1,We=e.RETURN_TRUSTED_TYPE||!1,He=e.FORCE_BODY||!1,Ge=!1!==e.SANITIZE_DOM,Ye=e.SANITIZE_NAMED_PROPS||!1,je=!1!==e.KEEP_CONTENT,Xe=e.IN_PLACE||!1,be=e.ALLOWED_URI_REGEXP||X,ot=e.NAMESPACE||nt,lt=e.MATHML_TEXT_INTEGRATION_POINTS||lt,ct=e.HTML_INTEGRATION_POINTS||ct,De=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(De.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(De.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(De.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Ie&&(xe=!1),Be&&(Fe=!0),qe&&(Ne=w({},U),we=[],!0===qe.html&&(w(Ne,L),w(we,z)),!0===qe.svg&&(w(Ne,C),w(we,P),w(we,F)),!0===qe.svgFilters&&(w(Ne,x),w(we,P),w(we,F)),!0===qe.mathMl&&(w(Ne,k),w(we,H),w(we,F))),e.ADD_TAGS&&(Ne===Re&&(Ne=D(Ne)),w(Ne,e.ADD_TAGS,pt)),e.ADD_ATTR&&(we===Oe&&(we=D(we)),w(we,e.ADD_ATTR,pt)),e.ADD_URI_SAFE_ATTR&&w(Je,e.ADD_URI_SAFE_ATTR,pt),e.FORBID_CONTENTS&&($e===Ke&&($e=D($e)),w($e,e.FORBID_CONTENTS,pt)),je&&(Ne["#text"]=!0),ze&&w(Ne,["html","head","body"]),Ne.table&&(w(Ne,["tbody"]),delete ve.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw b('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw b('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');le=e.TRUSTED_TYPES_POLICY,ce=le.createHTML("")}else void 0===le&&(le=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(j,c)),null!==le&&"string"==typeof ce&&(ce=le.createHTML(""));i&&i(e),ft=e}},Tt=w({},[...C,...x,...M]),yt=w({},[...k,...I]),Et=function(e){f(o.removed,{element:e});try{ae(e).removeChild(e)}catch(t){V(e)}},At=function(e,t){try{f(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){f(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(Fe||Be)try{Et(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},_t=function(e){let t=null,n=null;if(He)e=""+e;else{const t=T(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===ut&&ot===nt&&(e=''+e+"");const o=le?le.createHTML(e):e;if(ot===nt)try{t=(new Y).parseFromString(o,ut)}catch(e){}if(!t||!t.documentElement){t=se.createDocument(ot,"template",null);try{t.documentElement.innerHTML=rt?ce:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),ot===nt?pe.call(t,ze?"html":"body")[0]:ze?t.documentElement:i},St=function(e){return ue.call(e.ownerDocument||e,e,B.SHOW_ELEMENT|B.SHOW_COMMENT|B.SHOW_TEXT|B.SHOW_PROCESSING_INSTRUCTION|B.SHOW_CDATA_SECTION,null)},bt=function(e){return e instanceof G&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof W)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Nt=function(e){return"function"==typeof R&&e instanceof R};function Rt(e,t,n){u(e,(e=>{e.call(o,t,n,ft)}))}const wt=function(e){let t=null;if(Rt(de.beforeSanitizeElements,e,null),bt(e))return Et(e),!0;const n=pt(e.nodeName);if(Rt(de.uponSanitizeElement,e,{tagName:n,allowedTags:Ne}),e.hasChildNodes()&&!Nt(e.firstElementChild)&&S(/<[/\w]/g,e.innerHTML)&&S(/<[/\w]/g,e.textContent))return Et(e),!0;if(e.nodeType===ee)return Et(e),!0;if(Ue&&e.nodeType===te&&S(/<[/\w]/g,e.data))return Et(e),!0;if(!Ne[n]||ve[n]){if(!ve[n]&&Dt(n)){if(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,n))return!1;if(De.tagNameCheck instanceof Function&&De.tagNameCheck(n))return!1}if(je&&!$e[n]){const t=ae(e)||e.parentNode,n=ie(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=$(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,re(e))}}}return Et(e),!0}return e instanceof O&&!function(e){let t=ae(e);t&&t.tagName||(t={namespaceURI:ot,tagName:"template"});const n=h(e.tagName),o=h(t.tagName);return!!it[e.namespaceURI]&&(e.namespaceURI===tt?t.namespaceURI===nt?"svg"===n:t.namespaceURI===et?"svg"===n&&("annotation-xml"===o||lt[o]):Boolean(Tt[n]):e.namespaceURI===et?t.namespaceURI===nt?"math"===n:t.namespaceURI===tt?"math"===n&&ct[o]:Boolean(yt[n]):e.namespaceURI===nt?!(t.namespaceURI===tt&&!ct[o])&&!(t.namespaceURI===et&&!lt[o])&&!yt[n]&&(st[n]||!Tt[n]):!("application/xhtml+xml"!==ut||!it[e.namespaceURI]))}(e)?(Et(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!S(/<\/no(script|embed|frames)/i,e.innerHTML)?(Ie&&e.nodeType===Q&&(t=e.textContent,u([he,ge,Te],(e=>{t=y(t,e," ")})),e.textContent!==t&&(f(o.removed,{element:e.cloneNode()}),e.textContent=t)),Rt(de.afterSanitizeElements,e,null),!1):(Et(e),!0)},Ot=function(e,t,n){if(Ge&&("id"===t||"name"===t)&&(n in r||n in dt))return!1;if(xe&&!Le[t]&&S(ye,t));else if(Ce&&S(Ee,t));else if(!we[t]||Le[t]){if(!(Dt(e)&&(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,e)||De.tagNameCheck instanceof Function&&De.tagNameCheck(e))&&(De.attributeNameCheck instanceof RegExp&&S(De.attributeNameCheck,t)||De.attributeNameCheck instanceof Function&&De.attributeNameCheck(t))||"is"===t&&De.allowCustomizedBuiltInElements&&(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,n)||De.tagNameCheck instanceof Function&&De.tagNameCheck(n))))return!1}else if(Je[t]);else if(S(be,y(n,_e,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==E(n,"data:")||!Ve[e]){if(Me&&!S(Ae,y(n,_e,"")));else if(n)return!1}else;return!0},Dt=function(e){return"annotation-xml"!==e&&T(e,Se)},vt=function(e){Rt(de.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||bt(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:we,forceKeepAttr:void 0};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=pt(a);let m="value"===a?c:A(c);if(n.attrName=s,n.attrValue=m,n.keepAttr=!0,n.forceKeepAttr=void 0,Rt(de.uponSanitizeAttribute,e,n),m=n.attrValue,!Ye||"id"!==s&&"name"!==s||(At(a,e),m="user-content-"+m),Ue&&S(/((--!?|])>)|<\/(style|title)/i,m)){At(a,e);continue}if(n.forceKeepAttr)continue;if(At(a,e),!n.keepAttr)continue;if(!ke&&S(/\/>/i,m)){At(a,e);continue}Ie&&u([he,ge,Te],(e=>{m=y(m,e," ")}));const f=pt(e.nodeName);if(Ot(f,s,m)){if(le&&"object"==typeof j&&"function"==typeof j.getAttributeType)if(l);else switch(j.getAttributeType(f,s)){case"TrustedHTML":m=le.createHTML(m);break;case"TrustedScriptURL":m=le.createScriptURL(m)}try{l?e.setAttributeNS(l,a,m):e.setAttribute(a,m),bt(e)?Et(e):p(o.removed)}catch(e){}}}Rt(de.afterSanitizeAttributes,e,null)},Lt=function e(t){let n=null;const o=St(t);for(Rt(de.beforeSanitizeShadowDOM,t,null);n=o.nextNode();)Rt(de.uponSanitizeShadowNode,n,null),wt(n),vt(n),n.content instanceof s&&e(n.content);Rt(de.afterSanitizeShadowDOM,t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(rt=!e,rt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Nt(e)){if("function"!=typeof e.toString)throw b("toString is not a function");if("string"!=typeof(e=e.toString()))throw b("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Pe||gt(t),o.removed=[],"string"==typeof e&&(Xe=!1),Xe){if(e.nodeName){const t=pt(e.nodeName);if(!Ne[t]||ve[t])throw b("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof R)n=_t("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===J&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Fe&&!Ie&&!ze&&-1===e.indexOf("<"))return le&&We?le.createHTML(e):e;if(n=_t(e),!n)return Fe?null:We?ce:""}n&&He&&Et(n.firstChild);const c=St(Xe?e:n);for(;i=c.nextNode();)wt(i),vt(i),i.content instanceof s&&Lt(i.content);if(Xe)return e;if(Fe){if(Be)for(l=me.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(we.shadowroot||we.shadowrootmode)&&(l=fe.call(a,l,!0)),l}let m=ze?n.outerHTML:n.innerHTML;return ze&&Ne["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&S(K,n.ownerDocument.doctype.name)&&(m="\n"+m),Ie&&u([he,ge,Te],(e=>{m=y(m,e," ")})),le&&We?le.createHTML(m):m},o.setConfig=function(){gt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Pe=!0},o.clearConfig=function(){ft=null,Pe=!1},o.isValidAttribute=function(e,t,n){ft||gt({});const o=pt(e),r=pt(t);return Ot(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&f(de[e],t)},o.removeHook=function(e,t){if(void 0!==t){const n=m(de[e],t);return-1===n?void 0:d(de[e],n,1)[0]}return p(de[e])},o.removeHooks=function(e){de[e]=[]},o.removeAllHooks=function(){de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},o}();return re})); +//# sourceMappingURL=purify.min.js.map diff --git a/system3-nonconformance/web/static/js/pages/ai-assistant.js b/system3-nonconformance/web/static/js/pages/ai-assistant.js index d9492b8..deb1f48 100644 --- a/system3-nonconformance/web/static/js/pages/ai-assistant.js +++ b/system3-nonconformance/web/static/js/pages/ai-assistant.js @@ -196,7 +196,7 @@ function appendChatMessage(role, content, sources) { const contentDiv = document.createElement('div'); if (role === 'ai' && typeof marked !== 'undefined') { contentDiv.className = 'text-sm prose prose-sm max-w-none'; - contentDiv.innerHTML = marked.parse(content); + contentDiv.innerHTML = DOMPurify.sanitize(marked.parse(content)); } else { contentDiv.className = 'text-sm whitespace-pre-line'; contentDiv.textContent = content; diff --git a/system3-nonconformance/web/static/js/pages/issues-management.js b/system3-nonconformance/web/static/js/pages/issues-management.js index 09f1289..46e7664 100644 --- a/system3-nonconformance/web/static/js/pages/issues-management.js +++ b/system3-nonconformance/web/static/js/pages/issues-management.js @@ -990,7 +990,7 @@ async function aiSuggestSolution() { const raw = data.suggestion || ''; content.dataset.raw = raw; if (typeof marked !== 'undefined') { - content.innerHTML = marked.parse(raw); + content.innerHTML = DOMPurify.sanitize(marked.parse(raw)); } else { content.textContent = raw; } @@ -1030,7 +1030,7 @@ async function aiSuggestSolutionInline(issueId) { const raw = data.suggestion || ''; content.dataset.raw = raw; if (typeof marked !== 'undefined') { - content.innerHTML = marked.parse(raw); + content.innerHTML = DOMPurify.sanitize(marked.parse(raw)); } else { content.textContent = raw; } diff --git a/tkpurchase/api/index.js b/tkpurchase/api/index.js index 09c93cd..bb77b50 100644 --- a/tkpurchase/api/index.js +++ b/tkpurchase/api/index.js @@ -56,7 +56,7 @@ app.use((err, req, res, next) => { console.error('tkpurchase-api Error:', err.message); res.status(err.status || 500).json({ success: false, - error: err.message || 'Internal Server Error' + error: '서버 오류가 발생했습니다' }); }); diff --git a/tkpurchase/api/middleware/auth.js b/tkpurchase/api/middleware/auth.js index ceb33b8..35e7d74 100644 --- a/tkpurchase/api/middleware/auth.js +++ b/tkpurchase/api/middleware/auth.js @@ -25,9 +25,6 @@ function extractToken(req) { if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.split(' ')[1]; } - if (req.cookies && req.cookies.sso_token) { - return req.cookies.sso_token; - } return null; } diff --git a/tkpurchase/web/nginx.conf b/tkpurchase/web/nginx.conf index 44ca615..51a5d65 100644 --- a/tkpurchase/web/nginx.conf +++ b/tkpurchase/web/nginx.conf @@ -3,6 +3,10 @@ server { server_name _; charset utf-8; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + root /usr/share/nginx/html; index index.html; diff --git a/tksafety/api/index.js b/tksafety/api/index.js index 36178fc..fa28099 100644 --- a/tksafety/api/index.js +++ b/tksafety/api/index.js @@ -70,7 +70,7 @@ app.use((err, req, res, next) => { console.error('tksafety-api Error:', err.message); res.status(err.status || 500).json({ success: false, - error: err.message || 'Internal Server Error' + error: '서버 오류가 발생했습니다' }); }); diff --git a/tksafety/api/middleware/auth.js b/tksafety/api/middleware/auth.js index 9ad733e..c53e4c3 100644 --- a/tksafety/api/middleware/auth.js +++ b/tksafety/api/middleware/auth.js @@ -25,9 +25,6 @@ function extractToken(req) { if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.split(' ')[1]; } - if (req.cookies && req.cookies.sso_token) { - return req.cookies.sso_token; - } return null; } diff --git a/tksafety/web/nginx.conf b/tksafety/web/nginx.conf index 58428b3..2ac105f 100644 --- a/tksafety/web/nginx.conf +++ b/tksafety/web/nginx.conf @@ -2,6 +2,11 @@ server { listen 80; server_name _; charset utf-8; + + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + root /usr/share/nginx/html; index index.html; diff --git a/tksupport/api/controllers/vacationController.js b/tksupport/api/controllers/vacationController.js index cc211d3..e4386a8 100644 --- a/tksupport/api/controllers/vacationController.js +++ b/tksupport/api/controllers/vacationController.js @@ -246,6 +246,12 @@ const vacationController = { async getUserBalance(req, res) { try { const { userId } = req.params; + const requestedId = parseInt(userId); + const currentUserId = req.user.user_id || req.user.id; + const role = (req.user.role || '').toLowerCase(); + if (requestedId !== currentUserId && !['admin', 'system'].includes(role)) { + return res.status(403).json({ success: false, error: '접근 권한이 없습니다' }); + } const year = parseInt(req.query.year) || new Date().getFullYear(); const balances = await vacationBalanceModel.getByUserAndYear(userId, year); const hireDate = await vacationBalanceModel.getUserHireDate(userId); diff --git a/tksupport/api/index.js b/tksupport/api/index.js index 7a4a792..511fe58 100644 --- a/tksupport/api/index.js +++ b/tksupport/api/index.js @@ -46,7 +46,7 @@ app.use((err, req, res, next) => { console.error('tksupport-api Error:', err.message); res.status(err.status || 500).json({ success: false, - error: err.message || 'Internal Server Error' + error: '서버 오류가 발생했습니다' }); }); diff --git a/tksupport/api/middleware/auth.js b/tksupport/api/middleware/auth.js index 0226c41..6e39cd5 100644 --- a/tksupport/api/middleware/auth.js +++ b/tksupport/api/middleware/auth.js @@ -25,9 +25,6 @@ function extractToken(req) { if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.split(' ')[1]; } - if (req.cookies && req.cookies.sso_token) { - return req.cookies.sso_token; - } return null; } diff --git a/tksupport/web/index.html b/tksupport/web/index.html index 332b739..8f99914 100644 --- a/tksupport/web/index.html +++ b/tksupport/web/index.html @@ -120,7 +120,7 @@ - + + + +