From 81dcc2c15e5f9221f9516f262c2af9647649edeb Mon Sep 17 00:00:00 2001 From: O96u Date: Mon, 31 Aug 2026 15:02:42 +0800 Subject: [PATCH] =?UTF-8?q?release:=20v1.2.4=20=E2=80=94=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=9C=AA=E9=85=8D=E7=BD=AE=E9=98=B2=E7=9B=97=E9=93=BE?= =?UTF-8?q?=E6=97=B6=E5=A4=96=E9=93=BE=E5=9B=BE=E8=A3=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 +++ docs-site/en/guide/changelog.md | 6 +++ docs-site/guide/changelog.md | 6 +++ package-lock.json | 4 +- package.json | 2 +- server/utils/serve-image.test.ts | 68 +++++++++++++++++++++++++++++++- server/utils/serve-image.ts | 11 +++++- 7 files changed, 96 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe03fe..8c003cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ## [Unreleased] +## [1.2.4] - 2026-08-31 + +### 修复 + +- 访问控制(Referer 防盗链)留空时仍会拦截外链图片:浏览器直开无 Referer 可访问,博客/论坛嵌入带 Referer 会 403 图裂;现仅在配置了白名单时才校验 + ## [1.2.3] - 2026-08-30 ### 修复 diff --git a/docs-site/en/guide/changelog.md b/docs-site/en/guide/changelog.md index 00b41f3..218d6c3 100644 --- a/docs-site/en/guide/changelog.md +++ b/docs-site/en/guide/changelog.md @@ -4,6 +4,12 @@ Version history and notable changes. The full log is also in the repo root [CHAN --- +## [1.2.4] — 2026-08-31 + +### Fix + +- **Empty referer whitelist still blocked hotlinks**: with no Referer whitelist configured, direct browser access worked but embedded images on blogs/forums returned 403; protection now runs only when a whitelist is configured + ## [1.2.3] — 2026-08-30 ### Fix diff --git a/docs-site/guide/changelog.md b/docs-site/guide/changelog.md index d800f81..2e24c57 100644 --- a/docs-site/guide/changelog.md +++ b/docs-site/guide/changelog.md @@ -4,6 +4,12 @@ PicHost 版本历史与重要变更说明。完整记录亦见仓库根目录 [C --- +## [1.2.4] — 2026-08-31 + +### 修复 + +- **访问控制留空仍拦截外链**:未配置 Referer 白名单时,浏览器直开图片正常,但博客/论坛嵌入会因 Referer 校验返回 403 图裂;现仅在配置了白名单时才启用防盗链 + ## [1.2.3] — 2026-08-30 ### 修复 diff --git a/package-lock.json b/package-lock.json index a440a9a..efb74bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pichost", - "version": "1.2.3", + "version": "1.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pichost", - "version": "1.2.3", + "version": "1.2.4", "hasInstallScript": true, "license": "GPL-3.0-only", "dependencies": { diff --git a/package.json b/package.json index 8c6c544..09e9075 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pichost", - "version": "1.2.3", + "version": "1.2.4", "description": "Self-hosted image hosting for your NAS / home server — Nuxt admin UI, local disk or S3-compatible storage, server-side WebP compression, referer protection, Twikoo/EasyImage compatible.", "homepage": "https://github.com/O96u/PicHost#readme", "repository": { diff --git a/server/utils/serve-image.test.ts b/server/utils/serve-image.test.ts index 31306cf..866abfe 100644 --- a/server/utils/serve-image.test.ts +++ b/server/utils/serve-image.test.ts @@ -1,5 +1,30 @@ -import { describe, expect, it } from 'vitest' -import { isRefererAllowed } from './serve-image' +import { describe, expect, it, vi, afterEach } from 'vitest' +import type { H3Event } from 'h3' +import { isHotlinkBlocked, isRefererAllowed } from './serve-image' +import { getSetting, SETTINGS_ALLOWED_REFERER_HOSTS } from './db' + +vi.mock('./db', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + getSetting: vi.fn() + } +}) + +vi.stubGlobal('getRequestURL', () => new URL('https://pic.example.com')) + +vi.stubGlobal('useRuntimeConfig', () => ({ + apiUploadToken: '', + siteBaseUrl: '', + imageBaseUrl: '' +})) + +function mockEvent(): H3Event { + return { + node: { req: {}, res: {} }, + context: {} + } as H3Event +} describe('isRefererAllowed', () => { const allowedHosts = new Set(['pic.example.com', 'blog.example.com']) @@ -21,3 +46,42 @@ describe('isRefererAllowed', () => { expect(isRefererAllowed('not-a-url', allowedHosts)).toBe(false) }) }) + +describe('isHotlinkBlocked', () => { + afterEach(() => { + vi.clearAllMocks() + delete process.env.ALLOWED_REFERER_HOSTS + }) + + it('does not block external referers when protection is not configured', () => { + vi.mocked(getSetting).mockReturnValue(null) + const event = mockEvent() + expect(isHotlinkBlocked(event, 'https://blog.example.com/post')).toBe(false) + }) + + it('does not block external referers when db explicitly cleared', () => { + process.env.ALLOWED_REFERER_HOSTS = 'blog.example.com' + vi.mocked(getSetting).mockImplementation(key => + key === SETTINGS_ALLOWED_REFERER_HOSTS ? '' : null + ) + const event = mockEvent() + expect(isHotlinkBlocked(event, 'https://forum.example.com/thread')).toBe(false) + }) + + it('blocks external referers when protection is configured', () => { + vi.mocked(getSetting).mockImplementation(key => + key === SETTINGS_ALLOWED_REFERER_HOSTS ? 'blog.example.com' : null + ) + const event = mockEvent() + expect(isHotlinkBlocked(event, 'https://evil.example.com/hotlink')).toBe(true) + expect(isHotlinkBlocked(event, 'https://blog.example.com/post')).toBe(false) + }) + + it('still allows missing referer when protection is configured', () => { + vi.mocked(getSetting).mockImplementation(key => + key === SETTINGS_ALLOWED_REFERER_HOSTS ? 'blog.example.com' : null + ) + const event = mockEvent() + expect(isHotlinkBlocked(event, null)).toBe(false) + }) +}) diff --git a/server/utils/serve-image.ts b/server/utils/serve-image.ts index 2927d5b..9d4a411 100644 --- a/server/utils/serve-image.ts +++ b/server/utils/serve-image.ts @@ -4,7 +4,8 @@ import { getAllowedRefererHostsRaw, getImageBaseUrlConfigured, getSiteBaseUrlConfigured, - hostnameFromBaseUrl + hostnameFromBaseUrl, + isRefererConfigured } from './env' import { findImageKeyByDatePath, findImageKeyByFilename } from './image-index' import { validateImageKey } from './image-key' @@ -58,6 +59,12 @@ export function isRefererAllowed(referer: string | null, allowedHosts: Set