fix: CSP 策略移除 unsafe-inline,改用 nonce 机制
- SecurityHeaders 中间件生成随机 nonce,注入 context - BuildPageData/BuildAdminPageData 将 CSPNonce 传递给模板 - 所有 19 个模板文件 <script> 标签添加 nonce 属性 - 移除所有内联事件处理(onclick/onerror),改用事件监听 - 移除所有 javascript:void(0) 链接,改用 # 号 + preventDefault - utils.js 添加全局 avatar 图片加载失败回退处理 - common.js 登出按钮添加 preventDefault - audit.js 关闭弹窗按钮改用事件监听
This commit is contained in:
@ -42,7 +42,7 @@
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
||||
</button>
|
||||
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
<script src="/static/js/login.js?v={{assetV "/static/js/login.js"}}"></script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
<script src="/static/js/login.js?v={{assetV "/static/js/login.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
</label>
|
||||
<span class="remember-hint">非共享设备推荐</span>
|
||||
</div>
|
||||
<p class="guidelines-link">点击注册即表示同意 <a href="javascript:void(0)" id="openGuidelines">《MetaLab 社区用户准则》</a></p>
|
||||
<p class="guidelines-link">点击注册即表示同意 <a href="#" id="openGuidelines">《MetaLab 社区用户准则》</a></p>
|
||||
<button type="submit" class="submit-btn" id="submitBtn">注 册</button>
|
||||
</form>
|
||||
{{else}}
|
||||
@ -89,7 +89,7 @@
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
||||
</button>
|
||||
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
<script src="/static/js/register.js?v={{assetV "/static/js/register.js"}}"></script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
<script src="/static/js/register.js?v={{assetV "/static/js/register.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
<div class="sidebar-card user-card">
|
||||
{{if .IsLoggedIn}}
|
||||
{{if .Avatar}}
|
||||
<img src="{{.Avatar}}" alt="{{.Username}}" class="user-avatar-img" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
|
||||
<img src="{{.Avatar}}" alt="{{.Username}}" class="user-avatar-img">
|
||||
<div class="user-avatar-fallback" style="display:none">{{substr .Username 0 1}}</div>
|
||||
{{else}}
|
||||
<div class="user-avatar-fallback">{{substr .Username 0 1}}</div>
|
||||
@ -117,6 +117,6 @@
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -20,5 +20,5 @@
|
||||
{{if .ExtraCSS2}}
|
||||
<link rel="stylesheet" href="{{.ExtraCSS2}}?v={{assetV .ExtraCSS2}}">
|
||||
{{end}}
|
||||
<script src="/shared/static/js/utils.js?v={{assetV "/shared/static/js/utils.js"}}"></script>
|
||||
<script src="/shared/static/js/utils.js?v={{assetV "/shared/static/js/utils.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
</head>
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<li class="nav-avatar-wrapper">
|
||||
<a href="/space" class="nav-avatar-link">
|
||||
{{if .Avatar}}
|
||||
<img src="{{.Avatar}}" alt="" class="nav-avatar" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
|
||||
<img src="{{.Avatar}}" alt="" class="nav-avatar">
|
||||
<span class="nav-avatar nav-avatar-default" style="display:none">{{substr .Username 0 1}}</span>
|
||||
{{else}}
|
||||
<span class="nav-avatar nav-avatar-default">{{substr .Username 0 1}}</span>
|
||||
@ -30,7 +30,7 @@
|
||||
<div class="nav-dropdown-header">
|
||||
<div class="nav-dropdown-avatar">
|
||||
{{if .Avatar}}
|
||||
<img src="{{.Avatar}}" alt="" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
|
||||
<img src="{{.Avatar}}" alt="">
|
||||
<span style="display:none">{{substr .Username 0 1}}</span>
|
||||
{{else}}
|
||||
<span>{{substr .Username 0 1}}</span>
|
||||
@ -47,7 +47,7 @@
|
||||
<svg class="nav-dropdown-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<span>个人中心</span>
|
||||
</a>
|
||||
<a href="javascript:void(0)" class="nav-dropdown-item nav-logout" id="logoutBtn">
|
||||
<a href="#" class="nav-dropdown-item nav-logout" id="logoutBtn">
|
||||
<svg class="nav-dropdown-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||
<span>退出登录</span>
|
||||
</a>
|
||||
|
||||
@ -117,7 +117,7 @@
|
||||
</main>
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
<script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
@ -230,6 +230,6 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -17,6 +17,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{{template "layout/footer.html" .}}
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -50,6 +50,6 @@
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -146,10 +146,10 @@
|
||||
<textarea id="postMdContent" style="display:none">{{.Post.Body}}</textarea>
|
||||
|
||||
<link rel="stylesheet" href="/static/vditor/dist/index.css?v={{assetV "/static/vditor/dist/index.css"}}">
|
||||
<script src="/static/vditor/dist/method.min.js?v={{assetV "/static/vditor/dist/method.min.js"}}"></script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
<script src="/static/js/shortcode.js?v={{assetV "/static/js/shortcode.js"}}"></script>
|
||||
<script>
|
||||
<script src="/static/vditor/dist/method.min.js?v={{assetV "/static/vditor/dist/method.min.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
<script src="/static/js/shortcode.js?v={{assetV "/static/js/shortcode.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
(function() {
|
||||
var el = document.getElementById('postContent');
|
||||
var mdEl = document.getElementById('postMdContent');
|
||||
@ -180,7 +180,7 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
// 提交审核按钮
|
||||
(function() {
|
||||
var btn = document.getElementById('submitAuditBtn');
|
||||
@ -205,7 +205,7 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
// 赋能按钮 + 弹窗
|
||||
(function() {
|
||||
var btn = document.getElementById('energizeBtn');
|
||||
@ -287,7 +287,7 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
// ========== 赞/踩系统 ==========
|
||||
(function() {
|
||||
var container = document.getElementById('postReactions');
|
||||
@ -363,7 +363,7 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
// ========== 收藏按钮(弹窗选择收藏夹)==========
|
||||
(function() {
|
||||
var favBtn = document.getElementById('favBtn');
|
||||
@ -614,7 +614,7 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
// ========== 评论系统 ==========
|
||||
(function() {
|
||||
var commentsSection = document.getElementById('comments');
|
||||
|
||||
@ -328,8 +328,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
<script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
|
||||
@ -338,18 +338,18 @@
|
||||
{{if eq .ActiveTab "collections"}}
|
||||
<!-- 左侧收藏夹列表 -->
|
||||
<aside class="space-sidebar collection-sidebar">
|
||||
<h2 class="space-sidebar-title collapsible" onclick="toggleFolderSection(this)">
|
||||
<h2 class="space-sidebar-title collapsible" id="folderSectionToggle">
|
||||
我创建的收藏夹
|
||||
<svg class="collapse-arrow" viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
|
||||
</h2>
|
||||
<nav class="space-sidebar-nav" id="folder-nav">
|
||||
<a href="javascript:void(0)" class="space-sidebar-link new-folder-btn" onclick="showNewFolderInput()">
|
||||
<a href="#" class="space-sidebar-link new-folder-btn" id="showNewFolderBtn">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||
新建收藏夹
|
||||
</a>
|
||||
<div id="new-folder-input" style="display:none;">
|
||||
<input type="text" id="new-folder-name" placeholder="收藏夹名称" maxlength="50" class="new-folder-input">
|
||||
<button onclick="createFolder()" class="new-folder-submit">确定</button>
|
||||
<button id="createFolderBtn" class="new-folder-submit">确定</button>
|
||||
</div>
|
||||
{{if .FoldersResult}}
|
||||
{{range $f := .FoldersResult.Folders}}
|
||||
@ -576,11 +576,11 @@
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
|
||||
<!-- 访客空间:关注按钮交互 -->
|
||||
{{if and (not .IsOwnSpace) (or (eq .ActiveTab "following") (eq .ActiveTab "followers"))}}
|
||||
<script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
(function() {
|
||||
var cards = document.querySelectorAll('.space-user-card');
|
||||
if (!cards.length) return;
|
||||
@ -678,7 +678,7 @@
|
||||
|
||||
<!-- 收藏夹交互脚本 -->
|
||||
{{if eq .ActiveTab "collections"}}
|
||||
<script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
function toggleFolderSection(el) {
|
||||
el.parentElement.querySelector('#folder-nav').classList.toggle('collapsed');
|
||||
el.classList.toggle('collapsed');
|
||||
@ -701,6 +701,18 @@ function createFolder() {
|
||||
});
|
||||
}
|
||||
|
||||
// 事件绑定(替代内联 onclick,满足 CSP 无 unsafe-inline)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var toggle = document.getElementById('folderSectionToggle');
|
||||
if (toggle) toggle.addEventListener('click', function() { toggleFolderSection(this); });
|
||||
|
||||
var showBtn = document.getElementById('showNewFolderBtn');
|
||||
if (showBtn) showBtn.addEventListener('click', function(e) { e.preventDefault(); showNewFolderInput(); });
|
||||
|
||||
var createBtn = document.getElementById('createFolderBtn');
|
||||
if (createBtn) createBtn.addEventListener('click', function() { createFolder(); });
|
||||
});
|
||||
|
||||
// 隐私开关变更保存
|
||||
document.querySelectorAll('.privacy-settings-grid .toggle-switch input').forEach(function(cb) {
|
||||
cb.addEventListener('change', function() {
|
||||
@ -712,7 +724,7 @@ document.querySelectorAll('.privacy-settings-grid .toggle-switch input').forEach
|
||||
|
||||
<!-- 设置页交互脚本 -->
|
||||
{{if eq .ActiveTab "settings"}}
|
||||
<script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
// 隐私开关变更时自动保存到后端
|
||||
document.querySelectorAll('#privacy-follow-list, #privacy-follower-list').forEach(function(cb) {
|
||||
cb.addEventListener('change', function() {
|
||||
|
||||
@ -86,8 +86,8 @@
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
<script src="/static/js/chart.umd.min.js?v={{assetV "/static/js/chart.umd.min.js"}}"></script>
|
||||
<script>
|
||||
<script src="/static/js/chart.umd.min.js?v={{assetV "/static/js/chart.umd.min.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
|
||||
@ -28,8 +28,8 @@
|
||||
</div>
|
||||
<div class="post-item-actions">
|
||||
<a href="/studio/write?id={{.ID}}" class="action-btn primary">继续编辑</a>
|
||||
<button class="action-btn" onclick="submitPost({{.ID}})" title="提交发布">提交</button>
|
||||
<button class="action-btn danger" onclick="deletePost({{.ID}})" title="删除">删除</button>
|
||||
<button class="action-btn" data-post-id="{{.ID}}" data-action="submit" title="提交发布">提交</button>
|
||||
<button class="action-btn danger" data-post-id="{{.ID}}" data-action="delete" title="删除">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@ -57,7 +57,7 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
function submitPost(id) {
|
||||
if (!confirm('确定要提交这篇草稿进行审核吗?')) return;
|
||||
|
||||
@ -97,6 +97,19 @@ function deletePost(id) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 事件委托代替内联 onclick(满足 CSP 无 unsafe-inline)
|
||||
document.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
var id = btn.getAttribute('data-post-id');
|
||||
var action = btn.getAttribute('data-action');
|
||||
if (action === 'delete') {
|
||||
deletePost(id);
|
||||
} else if (action === 'submit') {
|
||||
submitPost(id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
<div class="post-item-actions">
|
||||
{{if not .IsLocked}}<a href="/studio/write?id={{.ID}}" class="action-btn">编辑</a>{{end}}
|
||||
<a href="/posts/{{.ID}}" target="_blank" class="action-btn">查看</a>
|
||||
<button class="action-btn danger" onclick="deletePost({{.ID}})" title="删除">删除</button>
|
||||
<button class="action-btn danger" data-post-id="{{.ID}}" data-action="delete" title="删除">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@ -78,7 +78,7 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
function deletePost(id) {
|
||||
if (!confirm('确定要删除这篇文章吗?')) return;
|
||||
|
||||
@ -98,6 +98,14 @@ function deletePost(id) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 事件委托代替内联 onclick(满足 CSP 无 unsafe-inline)
|
||||
document.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('[data-action="delete"]');
|
||||
if (btn) {
|
||||
deletePost(btn.getAttribute('data-post-id'));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
@ -95,8 +95,8 @@
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
<link rel="stylesheet" href="/static/vditor/dist/index.css?v={{assetV "/static/vditor/dist/index.css"}}">
|
||||
<script src="/static/vditor/dist/index.min.js?v={{assetV "/static/vditor/dist/index.min.js"}}"></script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
<script src="/static/js/studio-editor.js?v={{assetV "/static/js/studio-editor.js"}}"></script>
|
||||
<script src="/static/vditor/dist/index.min.js?v={{assetV "/static/vditor/dist/index.min.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
<script src="/static/js/studio-editor.js?v={{assetV "/static/js/studio-editor.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user