Harden admin security controls
This commit is contained in:
+27
-3
@@ -695,6 +695,7 @@ function toggleTheme() {
|
||||
applyTheme(saved || prefer);
|
||||
})();
|
||||
let isAdmin = false;
|
||||
let csrfToken = '';
|
||||
let profile = null;
|
||||
|
||||
async function init() {
|
||||
@@ -707,12 +708,21 @@ async function checkAuth() {
|
||||
const res = await fetch('api/auth.php?action=check');
|
||||
const data = await res.json();
|
||||
isAdmin = data.authenticated === true;
|
||||
csrfToken = data.csrf_token || '';
|
||||
document.querySelectorAll('.admin-controls').forEach(el => {
|
||||
el.classList.toggle('hidden', !isAdmin);
|
||||
});
|
||||
} catch (e) { isAdmin = false; }
|
||||
}
|
||||
|
||||
function csrfHeaders(headers = {}) {
|
||||
return csrfToken ? { ...headers, 'X-CSRF-Token': csrfToken } : headers;
|
||||
}
|
||||
|
||||
function csrfFetch(url, options = {}) {
|
||||
return fetch(url, { ...options, headers: csrfHeaders(options.headers || {}) });
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const res = await fetch('api/profile.php');
|
||||
@@ -734,7 +744,7 @@ function renderProfile() {
|
||||
|
||||
const avatarEl = document.getElementById('avatarEl');
|
||||
if (profile.avatar) {
|
||||
avatarEl.style.backgroundImage = `url('${escapeAttr(profile.avatar)}')`;
|
||||
avatarEl.style.backgroundImage = `url('${escapeAttr(sanitizeUrl(profile.avatar))}')`;
|
||||
avatarEl.style.backgroundSize = 'cover';
|
||||
avatarEl.style.backgroundPosition = 'center';
|
||||
avatarEl.innerHTML = '';
|
||||
@@ -818,8 +828,10 @@ function renderProfile() {
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
const safeHref = sanitizeUrl(c.href);
|
||||
if (!safeHref) return '';
|
||||
return `
|
||||
<a href="${escapeAttr(c.href)}" class="contact-card" target="_blank">
|
||||
<a href="${escapeAttr(safeHref)}" class="contact-card" target="_blank" rel="noopener noreferrer">
|
||||
<i class="${c.icon}"></i>
|
||||
<span class="label">${c.label}</span>
|
||||
<span class="value">${escapeHtml(c.value)}</span>
|
||||
@@ -1040,7 +1052,7 @@ async function saveProfile() {
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('api/profile.php', {
|
||||
const res = await csrfFetch('api/profile.php', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
@@ -1071,6 +1083,18 @@ function escapeHtml(str) {
|
||||
}
|
||||
function escapeAttr(str) { return escapeHtml(str); }
|
||||
|
||||
function sanitizeUrl(url) {
|
||||
if (!url) return '';
|
||||
const trimmed = String(url).trim();
|
||||
if (/^uploads\/[A-Za-z0-9가-힣._~!$&'()*+,;=:@%/-]+$/u.test(trimmed) &&
|
||||
!trimmed.includes('..') && !/(^|\/)\./.test(trimmed)) return trimmed;
|
||||
try {
|
||||
const parsed = new URL(trimmed, window.location.origin);
|
||||
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return trimmed;
|
||||
} catch (e) {}
|
||||
return '';
|
||||
}
|
||||
|
||||
// ESC 키로 모달 닫기 (바깥 클릭 닫기 제거)
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
|
||||
Reference in New Issue
Block a user