v1.10.88 품질·보안 개선과 미디어 카탈로그 database 전환을 반영한다.
세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
77
server/utils/csp-report-request-limit.js
Normal file
77
server/utils/csp-report-request-limit.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createError, setResponseHeader } from 'h3'
|
||||
import { getClientIp } from './request-ip.js'
|
||||
|
||||
const CSP_REPORT_WINDOW_MS = 60 * 1000
|
||||
const CSP_REPORT_REQUEST_LIMIT = 60
|
||||
const CSP_REPORT_BUCKET_LIMIT = 10000
|
||||
|
||||
/** @type {Map<string, { count: number, resetAt: number }>} */
|
||||
const cspReportBuckets = new Map()
|
||||
let lastCleanupAt = 0
|
||||
|
||||
/**
|
||||
* 원문 IP를 메모리에 남기지 않는 CSP 요청 제한 키를 만든다.
|
||||
* @param {import('h3').H3Event} event - 요청 이벤트
|
||||
* @returns {string} IP 해시 키
|
||||
*/
|
||||
const createCspReportLimitKey = (event) => createHash('sha256')
|
||||
.update(`csp-report:${getClientIp(event)}`)
|
||||
.digest('base64url')
|
||||
|
||||
/**
|
||||
* 만료되거나 상한을 넘은 CSP 요청 제한 버킷을 정리한다.
|
||||
* @param {number} now - 현재 시각
|
||||
* @returns {void}
|
||||
*/
|
||||
const cleanupCspReportBuckets = (now) => {
|
||||
if (now - lastCleanupAt < CSP_REPORT_WINDOW_MS && cspReportBuckets.size < CSP_REPORT_BUCKET_LIMIT) {
|
||||
return
|
||||
}
|
||||
|
||||
lastCleanupAt = now
|
||||
for (const [key, bucket] of cspReportBuckets) {
|
||||
if (bucket.resetAt <= now) {
|
||||
cspReportBuckets.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
while (cspReportBuckets.size >= CSP_REPORT_BUCKET_LIMIT) {
|
||||
const oldestKey = cspReportBuckets.keys().next().value
|
||||
if (!oldestKey) {
|
||||
break
|
||||
}
|
||||
cspReportBuckets.delete(oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 IP의 CSP 보고 요청이 분당 상한을 넘지 않았는지 확인한다.
|
||||
* @param {import('h3').H3Event} event - 요청 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
export const assertCspReportRequestAllowed = (event) => {
|
||||
const now = Date.now()
|
||||
const key = createCspReportLimitKey(event)
|
||||
cleanupCspReportBuckets(now)
|
||||
|
||||
const current = cspReportBuckets.get(key)
|
||||
if (!current || current.resetAt <= now) {
|
||||
cspReportBuckets.set(key, {
|
||||
count: 1,
|
||||
resetAt: now + CSP_REPORT_WINDOW_MS
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
current.count += 1
|
||||
if (current.count <= CSP_REPORT_REQUEST_LIMIT) {
|
||||
return
|
||||
}
|
||||
|
||||
setResponseHeader(event, 'Retry-After', String(Math.max(1, Math.ceil((current.resetAt - now) / 1000))))
|
||||
throw createError({
|
||||
statusCode: 429,
|
||||
message: 'CSP 보고 요청이 너무 많습니다.'
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user