Commit f252bc73 by Archer Committed by GitHub

perf: ip check (#6850)

* perf: ip check

* fix: restore IP limit proxy handling

* test: check xff and xri

* fix: type check

* doc

* update template

* fix: workspace

* fix: import

* fix: align IP limit env usage

* init env

* init env

* fix: mongo log url

* fix: mongo log url

* submodule

* update piagent import

---------

Co-authored-by: xqvvu <whoeverimf5@gmail.com>
parent 89b80f75
......@@ -14,17 +14,32 @@ description: 'FastGPT V4.15.0 更新说明'
`fastgpt-app`, `fastgpt-pro` 可增加文件解析并发线程数
```bash
# 文件解析 worker 并发数
```dotenv
# 文件解析 worker 并发数(可选)
PARSE_FILE_WORKERS=10
# 文件解析超时时间(秒)
# 文件解析超时时间(秒)(可选)
PARSE_FILE_TIMEOUT_SECONDS=600
# HTML 转 Markdown worker 并发数
# HTML 转 Markdown worker 并发数(可选)
HTML_TO_MARKDOWN_WORKERS=10
# 文本切块 worker 并发数
# 文本切块 worker 并发数(可选)
TEXT_TO_CHUNKS_WORKERS=10
# 自动同步 mongo 数据库索引, 改成 boolean 字符串值,而不是 0 和 1
# 自动同步 mongo 数据库索引, 改成 boolean 字符串值,而不是 0 和 1(可选)
SYNC_INDEX=true
# 是否启用可信反向代理客户端 IP 校验(可选)
TRUSTED_PROXY_ENABLE=false
# 可信反向代理 IP/CIDR 列表,逗号或空白分隔。仅 TRUSTED_PROXY_ENABLE=true 时生效;仅显式可信代理传入的 X-Forwarded-For/X-Real-IP 会用于客户端 IP 解析(可选)
TRUSTED_PROXY_IPS=
```
### 确认是否遗漏环境变量
本次升级,增加了对于环境变量的检测,避免漏填必须的环境变量,需重点检查`fastgpt-app`和`fastgpt-pro`是否包含:
```dotenv
# 密钥加密密钥,两个服务需一致
AES256_SECRET_KEY=
# 文件 token 密钥,两个服务需一致
FILE_TOKEN_KEY=
```
## 🚀 新增内容
......@@ -45,6 +60,7 @@ SYNC_INDEX=true
5. 加强第三方知识库请求的 SSRF 防护。
6. codex-sandbox 加强 AST 检查,防止绕过安全检查。
7. 站点同步限流错误提示,重复提示。
8. 加强 IP 检测,避免伪造绕过。
## 🐛 修复
......
......@@ -145,8 +145,8 @@
"content/openapi/intro.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/share.en.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/share.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/config/env.en.mdx": "2026-04-30T00:20:04+08:00",
"content/self-host/config/env.mdx": "2026-04-30T00:20:04+08:00",
"content/self-host/config/env.en.mdx": "2026-05-06T18:25:24+08:00",
"content/self-host/config/env.mdx": "2026-05-06T18:25:24+08:00",
"content/self-host/config/json.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/config/json.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/config/model/intro.en.mdx": "2026-04-26T21:08:47+08:00",
......@@ -257,7 +257,7 @@
"content/self-host/upgrading/4-14/41481.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/4149.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/4149.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-15/4150.mdx": "2026-05-06T14:03:59+08:00",
"content/self-host/upgrading/4-15/4150.mdx": "2026-05-06T18:31:58+08:00",
"content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00",
......@@ -398,8 +398,8 @@
"content/self-host/upgrading/outdated/499.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/upgrade-intruction.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/upgrade-intruction.mdx": "2026-04-26T21:08:47+08:00",
"content/toc.en.mdx": "2026-04-29T23:22:43+08:00",
"content/toc.mdx": "2026-04-29T23:22:43+08:00",
"content/toc.en.mdx": "2026-05-06T18:25:24+08:00",
"content/toc.mdx": "2026-05-06T18:25:24+08:00",
"content/use-cases/app-cases/dalle3.en.mdx": "2026-04-26T21:08:47+08:00",
"content/use-cases/app-cases/dalle3.mdx": "2026-04-26T21:08:47+08:00",
"content/use-cases/app-cases/english_essay_correction_bot.en.mdx": "2026-04-26T21:08:47+08:00",
......
......@@ -14,6 +14,7 @@
"@apidevtools/swagger-parser": "^10.1.0",
"@bany/curl-to-json": "^1.2.8",
"axios": "catalog:",
"ipaddr.js": "catalog:",
"cron-parser": "^4.9.0",
"dayjs": "catalog:",
"encoding": "^0.1.13",
......@@ -24,7 +25,6 @@
"next": "catalog:",
"openai": "6.34.0",
"openapi-types": "^12.1.3",
"ipaddr.js": "^2.3.0",
"timezones-list": "^3.0.2",
"lodash": "catalog:",
"zod": "catalog:",
......
......@@ -6,10 +6,10 @@ import type { LocationName } from './type';
import { extractLocationData } from './utils';
import type { NextApiRequest } from 'next';
export type { NextApiRequest } from 'next';
import { getClientIp } from 'request-ip';
import { getLogger } from '../logger';
import type { localeType } from '@fastgpt/global/common/i18n/type';
import { formatI18nLocationToZhEn } from '@fastgpt/global/common/i18n/utils';
import { getClientIpFromRequest } from '../security/clientIp';
const logger = getLogger(['GEO']);
......@@ -109,7 +109,7 @@ export function initGeo() {
}
export function getIpFromRequest(request: NextApiRequest): string {
const ip = getClientIp(request);
const ip = getClientIpFromRequest(request);
if (!ip || ip === '::1') {
return '127.0.0.1';
}
......
......@@ -7,6 +7,7 @@ import { getLogger, LogCategories, withContext } from '../logger';
import { setSpanError, withActiveSpan } from '../tracing';
import { ZodError } from 'zod';
import { randomUUID } from 'crypto';
import { getClientIpFromRequest } from '../security/clientIp';
export type NextApiHandler<T = any> = (
req: ApiRequestProps,
......@@ -61,7 +62,7 @@ export const NextEntry = ({
const url = req.url || '';
const route = getRequestRoute(url);
const method = req.method?.toUpperCase() || '';
const ip = req.headers['x-forwarded-for'] || req.socket?.remoteAddress;
const ip = getClientIpFromRequest(req);
const userAgent = req.headers['user-agent'];
const contentLength = req.headers['content-length'];
const requestBodySize = parseHeaderNumber(contentLength);
......
import { type ApiRequestProps } from '../../type/next';
import requestIp from 'request-ip';
import { authFrequencyLimit } from '../system/frequencyLimit/utils';
import { addSeconds } from 'date-fns';
import { type NextApiResponse } from 'next';
import { jsonRes } from '../response';
import { serviceEnv } from '../../env';
import { getClientIpFromRequest } from '../security/clientIp';
// unit: times/s
// how to use?
......@@ -21,10 +21,11 @@ export function useIPFrequencyLimit({
force?: boolean;
}) {
return async (req: ApiRequestProps, res: NextApiResponse) => {
const ip = requestIp.getClientIp(req);
if (!ip || (!serviceEnv.USE_IP_LIMIT && !force)) {
if (!serviceEnv.USE_IP_LIMIT && !force) {
return;
}
const ip = getClientIpFromRequest(req) ?? 'unknown';
try {
await authFrequencyLimit({
eventId: `ip-qps-limit-${id}-` + ip,
......
import type { IncomingHttpHeaders, IncomingMessage } from 'http';
import ipaddr from 'ipaddr.js';
import proxyaddr from 'proxy-addr';
import { serviceEnv } from '../../env';
type IPAddress = ipaddr.IPv4 | ipaddr.IPv6;
type RequestWithClientIp = {
headers?: IncomingHttpHeaders;
socket?: {
remoteAddress?: string | null;
};
connection?: {
remoteAddress?: string | null;
};
};
type TrustProxyFn = (addr: string, i: number) => boolean;
const MAX_FORWARDED_FOR_LENGTH = 2048;
const MAX_FORWARDED_FOR_HOPS = 32;
let cachedTrustedProxyIpEnv: string | undefined | null = null;
let cachedTrustedProxyEnableEnv: boolean | undefined | null = null;
let cachedNodeEnv: string | undefined | null = null;
let cachedTrustProxyFn: TrustProxyFn = proxyaddr.compile([]);
let warnedInvalidTrustedProxyIpEnv: string | undefined;
// 不区分大小写读取 header 值;数组类型(如 set-cookie 风格)合并为逗号分隔字符串。
const getHeaderValue = (headers: IncomingHttpHeaders | undefined, key: string) => {
const value =
headers?.[key] ??
Object.entries(headers ?? {}).find(([headerKey]) => headerKey.toLowerCase() === key)?.[1];
if (Array.isArray(value)) return value.join(',');
return value;
};
// 剥离 IP 字符串外层的引号、IPv6 方括号以及 IPv4/IPv6 末尾的端口,返回纯地址。
const stripIpWrapper = (rawIp: string) => {
const ip = rawIp.trim().replace(/^"(.+)"$/, '$1');
const bracketedIpv6 = ip.match(/^\[([^\]]+)](?::\d+)?$/);
if (bracketedIpv6?.[1]) return bracketedIpv6[1];
const ipv4WithPort = ip.match(/^(\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?$/);
if (ipv4WithPort?.[1]) return ipv4WithPort[1];
return ip;
};
// 将原始字符串解析为 ipaddr.js 的地址对象;非法或为空时返回 null,内部走 ipaddr.process 以归一 IPv4-mapped IPv6。
const parseIpAddress = (rawIp?: string | null): IPAddress | null => {
if (!rawIp) return null;
const ip = stripIpWrapper(rawIp);
if (!ipaddr.isValid(ip)) return null;
try {
return ipaddr.process(ip);
} catch {
return null;
}
};
// 校验单条 TRUSTED_PROXY_IPS 配置项是否为合法的 IP 或 CIDR(校验掩码长度与地址族匹配)。
const isValidTrustedProxyAddress = (rawValue: string) => {
const addressParts = rawValue.trim().split('/');
if (addressParts.length > 2) return false;
const [rawAddress, rawPrefixLength] = addressParts;
const address = parseIpAddress(rawAddress);
if (!address) return false;
if (rawPrefixLength === undefined) return true;
const prefixLength = Number(rawPrefixLength);
const maxLength = address.kind() === 'ipv4' ? 32 : 128;
return Number.isInteger(prefixLength) && prefixLength > 0 && prefixLength <= maxLength;
};
// 按逗号/空白拆分 TRUSTED_PROXY_IPS,过滤非法项并去重打印一次警告;非 test 环境下提示运维。
const parseTrustedProxyIpEnv = (trustedProxyIpEnv?: string) => {
const validAddresses = new Set<string>();
const invalidAddresses = new Set<string>();
(trustedProxyIpEnv ?? '')
.split(/[,\s]+/)
.filter(Boolean)
.forEach((item) => {
if (isValidTrustedProxyAddress(item)) {
validAddresses.add(item);
} else {
invalidAddresses.add(item);
}
});
if (
invalidAddresses.size > 0 &&
process.env.NODE_ENV !== 'test' &&
warnedInvalidTrustedProxyIpEnv !== trustedProxyIpEnv
) {
warnedInvalidTrustedProxyIpEnv = trustedProxyIpEnv;
console.warn(
`[security:client-ip] Ignored invalid TRUSTED_PROXY_IPS entries: ${Array.from(
invalidAddresses
).join(', ')}`
);
}
return Array.from(validAddresses);
};
// 构建并缓存 proxy-addr 的信任判定函数;用于 TRUSTED_PROXY_ENABLE=true 的可信代理校验模式。
// 可信代理校验模式下,非生产环境默认信任 loopback,并叠加 TRUSTED_PROXY_IPS 配置。
// 仅当环境变量或 NODE_ENV 变化时才重新编译,避免每次请求都重复解析。
const getTrustProxyFn = () => {
const trustedProxyEnable = serviceEnv.TRUSTED_PROXY_ENABLE;
const trustedProxyIpEnv = serviceEnv.TRUSTED_PROXY_IPS;
const nodeEnv = process.env.NODE_ENV;
if (
trustedProxyEnable === cachedTrustedProxyEnableEnv &&
trustedProxyIpEnv === cachedTrustedProxyIpEnv &&
nodeEnv === cachedNodeEnv
) {
return cachedTrustProxyFn;
}
cachedTrustedProxyEnableEnv = trustedProxyEnable;
cachedTrustedProxyIpEnv = trustedProxyIpEnv;
cachedNodeEnv = nodeEnv;
const trustedProxyAddresses = trustedProxyEnable
? [
...(nodeEnv === 'production' ? [] : (['loopback'] satisfies proxyaddr.Address[])),
...parseTrustedProxyIpEnv(trustedProxyIpEnv)
]
: [];
cachedTrustProxyFn = proxyaddr.compile(trustedProxyAddresses);
return cachedTrustProxyFn;
};
// 将地址对象转为小写字符串,统一 IPv6 大小写写法以便比较。
const normalizeIpAddress = (address: IPAddress) => address.toString().toLowerCase();
// 对外:把任意来源的 IP 字符串解析并归一化(去端口/方括号、小写),非法返回 undefined。
export const normalizeClientIp = (rawIp?: string | null) => {
const address = parseIpAddress(rawIp);
if (!address) return;
return normalizeIpAddress(address);
};
// 对外:判断给定 IP 是否在受信代理白名单内,供上游中间件决定是否采纳转发头。
export const isTrustedProxyIp = (rawIp?: string | null) => {
const ip = normalizeClientIp(rawIp);
if (!ip) return false;
return getTrustProxyFn()(ip, 0);
};
// 取 TCP 连接对端地址(socket / 旧版 connection 兜底),作为最可信的回退来源。
const getRemoteIp = (req: RequestWithClientIp) => {
return normalizeClientIp(req.socket?.remoteAddress ?? req.connection?.remoteAddress);
};
// 读取并归一化 X-Real-IP 头;通常由 Nginx 等单层代理设置为最初客户端 IP。
const getClientIpFromRealIp = (req: RequestWithClientIp) => {
const xRealIp = getHeaderValue(req.headers, 'x-real-ip');
return normalizeClientIp(xRealIp);
};
// 读取原始 X-Forwarded-For 头(不解析、不归一),后续校验和 proxy-addr 解析使用。
const getForwardedFor = (req: RequestWithClientIp) => {
return getHeaderValue(req.headers, 'x-forwarded-for');
};
// 关闭可信代理校验时的兼容模式:直接相信转发头。
// X-Forwarded-For 按行业约定取最左侧 IP;如果不存在或非法,再尝试 X-Real-IP。
const getClientIpFromForwardingHeaders = (req: RequestWithClientIp) => {
const forwardedFor = getForwardedFor(req);
if (forwardedFor && forwardedFor.length <= MAX_FORWARDED_FOR_LENGTH) {
const firstForwardedIp = forwardedFor.split(',')[0]?.trim();
const ip = normalizeClientIp(firstForwardedIp);
if (ip) return ip;
}
return getClientIpFromRealIp(req);
};
// 在调用 proxy-addr 前对 XFF 做尺寸/跳数/格式预检,防止超长或畸形头造成解析放大攻击。
const isForwardedForSafeToParse = (forwardedFor: string) => {
if (forwardedFor.length > MAX_FORWARDED_FOR_LENGTH) return false;
const hops = forwardedFor.split(',').map((hop) => hop.trim());
return (
hops.length > 0 &&
hops.length <= MAX_FORWARDED_FOR_HOPS &&
hops.every((hop) => Boolean(normalizeClientIp(hop)))
);
};
// 构造一个最小化的 IncomingMessage 形状对象供 proxy-addr 使用:
// 仅保留经过调用方校验的 XFF 头与指定 remoteAddress,避免外部 header 干扰判定。
const createProxyAddrRequest = (remoteAddress: string, forwardedFor: string) => {
return {
headers: {
'x-forwarded-for': forwardedFor
},
socket: {
remoteAddress
}
} as unknown as IncomingMessage;
};
// 对外:从请求中解析出最终客户端 IP。
// 策略:
// 1. TRUSTED_PROXY_ENABLE=false -> 兼容模式,直接相信 X-Forwarded-For / X-Real-IP,再回退远端 IP。
// 2. TRUSTED_PROXY_ENABLE=true -> 可信代理校验模式,先取 socket 远端 IP 作为底线;若不可解析直接返回 undefined。
// 3. 远端不在受信代理列表 -> 直接返回远端 IP,忽略一切转发头(防伪造)。
// 4. 远端可信 -> 优先用 X-Forwarded-For(经安全校验后交给 proxy-addr 沿信任链回溯),
// 否则回退 X-Real-IP;校验失败或转发头本身仍是受信代理时退回远端 IP。
export const getClientIpFromRequest = (req: RequestWithClientIp) => {
if (!serviceEnv.TRUSTED_PROXY_ENABLE) {
return getClientIpFromForwardingHeaders(req) ?? getRemoteIp(req);
}
const remoteIp = getRemoteIp(req);
if (!remoteIp) return;
const trustProxy = getTrustProxyFn();
if (trustProxy(remoteIp, 0)) {
const forwardedFor = getForwardedFor(req);
if (forwardedFor) {
if (!isForwardedForSafeToParse(forwardedFor)) {
return remoteIp;
}
const forwardedIp = normalizeClientIp(
proxyaddr(createProxyAddrRequest(remoteIp, forwardedFor), trustProxy)
);
if (forwardedIp && forwardedIp !== remoteIp && !trustProxy(forwardedIp, 0)) {
return forwardedIp;
}
return remoteIp;
}
const realIp = getClientIpFromRealIp(req);
return realIp && realIp !== remoteIp && !trustProxy(realIp, 0) ? realIp : remoteIp;
}
return remoteIp;
};
......@@ -30,9 +30,13 @@ export const authFrequencyLimit = async ({
).lean();
// 因为始终会返回+1的结果,所以这里不能直接等,需要多一个。
if (result.amount > maxAmount) {
return Promise.reject(ERROR_ENUM.uploadFileIntervalLimit);
throw ERROR_ENUM.tooManyRequest;
}
} catch (error) {
if (error === ERROR_ENUM.tooManyRequest) {
throw error;
}
logger.error('Failed to update auth frequency limit', { eventId, error });
}
};
......@@ -25,6 +25,7 @@ import { buildAgentTools } from './toolAdapter';
import { getLogger, LogCategories } from '../../../../../../common/logger';
import { serviceEnv } from '../../../../../../env';
import type { DispatchAgentModuleProps } from '..';
import { Agent, type AgentEvent } from '@mariozechner/pi-agent-core';
type Response = DispatchNodeResultType<{
[NodeOutputKeyEnum.answerText]: string;
......@@ -191,8 +192,6 @@ export const dispatchPiAgent = async (props: DispatchAgentModuleProps): Promise<
: [];
/* ===== Create & run Agent ===== */
const { Agent } = await import('@mariozechner/pi-agent-core');
type AgentEvent = import('@mariozechner/pi-agent-core').AgentEvent;
const agent = new Agent({
initialState: {
......
......@@ -103,12 +103,7 @@ export const serviceEnv = createEnv({
.default(
'mongodb://myusername:mypassword@localhost:27017/fastgpt?authSource=admin&directConnection=true'
),
MONGODB_LOG_URI: z
.string()
.default(
'mongodb://myusername:mypassword@localhost:27017/fastgpt?authSource=admin&directConnection=true'
)
.optional(),
MONGODB_LOG_URI: z.string().optional(),
// VectorDB
VECTOR_VQ_LEVEL: IntSchema.default(32).meta({
......@@ -175,6 +170,14 @@ export const serviceEnv = createEnv({
//==================== 安全配置 ====================
USE_IP_LIMIT: BoolSchema.default(false).meta({ description: '是否启用 IP 限流' }),
CHECK_INTERNAL_IP: BoolSchema.default(false).meta({ description: '是否启用内网 IP 检查' }),
TRUSTED_PROXY_ENABLE: BoolSchema.default(false).meta({
description:
'是否启用可信反向代理客户端 IP 校验;关闭时兼容旧逻辑,直接信任 X-Forwarded-For/X-Real-IP'
}),
TRUSTED_PROXY_IPS: z.string().optional().meta({
description:
'可信反向代理 IP/CIDR 列表,逗号或空白分隔。仅 TRUSTED_PROXY_ENABLE=true 时生效;仅显式可信代理传入的 X-Forwarded-For/X-Real-IP 会用于客户端 IP 解析'
}),
PASSWORD_LOGIN_LOCK_SECONDS: defaultableIntSchema(120).meta({
description: '密码错误锁定时长(秒)'
}),
......@@ -187,7 +190,6 @@ export const serviceEnv = createEnv({
description: '是否强制将图片转成 base64 传递给模型'
}),
// ==================== 功能开关与特殊配置 ====================
//==================== Beta features ====================
SHOW_SKILL: BoolSchema.default(false).meta({ description: '是否展示 Skill 功能入口' }),
AGENT_ENGINE: z
......
......@@ -3,18 +3,11 @@ import type {
LicenseDataType,
SystemEnvType
} from '@fastgpt/global/common/system/types';
import {
TTSModelType,
RerankModelItemType,
STTModelType,
EmbeddingModelItemType,
LLMModelItemType
} from '@fastgpt/global/core/ai/model.schema';
import type { SubPlanType } from '@fastgpt/global/support/wallet/sub/type';
import type { WorkerNameEnum, WorkerPool } from './worker/utils';
import { Worker } from 'worker_threads';
declare global {
var countTrackQueue: Map<string, { event: string; count: number; data: Record<string, any> }>;
var systemInitBufferId: string | undefined;
var systemVersion: string;
......@@ -25,3 +18,5 @@ declare global {
var workerPoll: Record<WorkerNameEnum, WorkerPool>;
}
export {};
......@@ -44,6 +44,7 @@
"iconv-lite": "^0.6.3",
"ioredis": "^5.6.0",
"joplin-turndown-plugin-gfm": "^1.0.12",
"ipaddr.js": "catalog:",
"json5": "catalog:",
"jsonpath-plus": "^10.3.0",
"jsonrepair": "^3.0.0",
......@@ -67,9 +68,9 @@
"pg": "^8.10.0",
"pino": "^9.7.0",
"pino-opentelemetry-transport": "^1.0.1",
"proxy-addr": "catalog:",
"proxy-agent": "catalog:",
"proxy-from-env": "^1.1.0",
"request-ip": "catalog:",
"tiktoken": "1.0.17",
"turndown": "^7.1.2",
"undici": "^7.18.2",
......@@ -87,8 +88,8 @@
"@types/node-cron": "^3.0.11",
"@types/papaparse": "5.3.7",
"@types/pg": "^8.6.6",
"@types/proxy-addr": "catalog:",
"@types/proxy-from-env": "^1.0.4",
"@types/request-ip": "catalog:",
"@types/tunnel": "^0.0.4",
"@types/turndown": "^5.0.4"
}
......
......@@ -8,6 +8,17 @@ import {
type NextApiRequest
} from '@fastgpt/service/common/geo';
import { cleanupIntervalMs } from '@fastgpt/service/common/geo/constants';
import { serviceEnv } from '@fastgpt/service/env';
const originalTrustedProxyEnable = serviceEnv.TRUSTED_PROXY_ENABLE;
const setTrustedProxyEnable = (value: boolean) => {
serviceEnv.TRUSTED_PROXY_ENABLE = value;
};
afterEach(() => {
setTrustedProxyEnable(originalTrustedProxyEnable);
});
describe('getGeoReader', () => {
it('should return a reader instance', () => {
......@@ -174,10 +185,12 @@ describe('getIpFromRequest', () => {
});
it('should return the IP from x-forwarded-for header', () => {
setTrustedProxyEnable(true);
const req = {
headers: { 'x-forwarded-for': '203.0.113.50' },
connection: {},
socket: {}
socket: { remoteAddress: '127.0.0.1' }
} as unknown as NextApiRequest;
const ip = getIpFromRequest(req);
......@@ -185,13 +198,28 @@ describe('getIpFromRequest', () => {
});
it('should return the IP from x-real-ip header', () => {
setTrustedProxyEnable(true);
const req = {
headers: { 'x-real-ip': '198.51.100.10' },
connection: {},
socket: {}
socket: { remoteAddress: '127.0.0.1' }
} as unknown as NextApiRequest;
const ip = getIpFromRequest(req);
expect(ip).toBe('198.51.100.10');
});
it('should ignore spoofed IP headers from untrusted direct clients', () => {
setTrustedProxyEnable(true);
const req = {
headers: { 'x-forwarded-for': '203.0.113.50', 'x-real-ip': '198.51.100.10' },
connection: {},
socket: { remoteAddress: '192.0.2.20' }
} as unknown as NextApiRequest;
const ip = getIpFromRequest(req);
expect(ip).toBe('192.0.2.20');
});
});
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
import { MongoFrequencyLimit } from '@fastgpt/service/common/system/frequencyLimit/schema';
import { jsonRes } from '@fastgpt/service/common/response';
import { serviceEnv } from '@fastgpt/service/env';
const originalUseIpLimit = serviceEnv.USE_IP_LIMIT;
const originalTrustedProxyEnable = serviceEnv.TRUSTED_PROXY_ENABLE;
const setUseIpLimit = (value: boolean) => {
serviceEnv.USE_IP_LIMIT = value;
};
const setTrustedProxyEnable = (value: boolean) => {
serviceEnv.TRUSTED_PROXY_ENABLE = value;
};
const createRes = () =>
({
setHeader: vi.fn(),
status: vi.fn().mockReturnThis(),
json: vi.fn(),
end: vi.fn()
}) as any;
const createReq = ({
headers = {},
remoteAddress
}: {
headers?: Record<string, string>;
remoteAddress?: string;
}) =>
({
headers,
socket: {
remoteAddress
}
}) as any;
describe('useIPFrequencyLimit', () => {
beforeEach(async () => {
vi.clearAllMocks();
await MongoFrequencyLimit.deleteMany({
eventId: /^ip-qps-limit-ip-spoof-test-/
});
});
afterEach(() => {
setUseIpLimit(originalUseIpLimit);
setTrustedProxyEnable(originalTrustedProxyEnable);
});
it('should enforce IP limit when USE_IP_LIMIT is enabled without force', async () => {
setUseIpLimit(true);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-toggle-enabled',
seconds: 60,
limit: 10
});
await middleware(
createReq({
remoteAddress: '198.51.100.40'
}),
createRes()
);
const record = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-toggle-enabled-198.51.100.40'
}).lean();
expect(record?.amount).toBe(1);
});
it('should skip IP limit when USE_IP_LIMIT is disabled without force', async () => {
setUseIpLimit(false);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-toggle-disabled',
seconds: 60,
limit: 10
});
await middleware(
createReq({
remoteAddress: '198.51.100.41'
}),
createRes()
);
const record = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-toggle-disabled-198.51.100.41'
}).lean();
expect(record).toBeNull();
});
it('should enforce IP limit when force is true even if USE_IP_LIMIT is disabled', async () => {
setUseIpLimit(false);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-toggle-forced',
seconds: 60,
limit: 10,
force: true
});
await middleware(
createReq({
remoteAddress: '198.51.100.42'
}),
createRes()
);
const record = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-toggle-forced-198.51.100.42'
}).lean();
expect(record?.amount).toBe(1);
});
it('should ignore spoofed forwarding headers from untrusted direct clients', async () => {
setTrustedProxyEnable(true);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-direct',
seconds: 60,
limit: 10,
force: true
});
await middleware(
createReq({
remoteAddress: '198.51.100.20',
headers: {
'x-forwarded-for': '203.0.113.50',
'x-real-ip': '203.0.113.51'
}
}),
createRes()
);
const realIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-direct-198.51.100.20'
}).lean();
const spoofedIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-direct-203.0.113.50'
}).lean();
expect(realIpRecord?.amount).toBe(1);
expect(spoofedIpRecord).toBeNull();
});
it('should use X-Forwarded-For as the limit key when trusted proxy parsing is disabled', async () => {
setTrustedProxyEnable(false);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-compat',
seconds: 60,
limit: 10,
force: true
});
await middleware(
createReq({
remoteAddress: '172.16.0.119',
headers: {
'x-forwarded-for': '60.186.209.23',
'x-real-ip': '60.186.209.23'
}
}),
createRes()
);
const forwardedIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-compat-60.186.209.23'
}).lean();
const remoteIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-compat-172.16.0.119'
}).lean();
expect(forwardedIpRecord?.amount).toBe(1);
expect(remoteIpRecord).toBeNull();
});
it('should use proxy-addr result for trusted proxy forwarding chains', async () => {
setTrustedProxyEnable(true);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-proxy',
seconds: 60,
limit: 10,
force: true
});
await middleware(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': '6.6.6.6, 203.0.113.50'
}
}),
createRes()
);
const clientIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-proxy-203.0.113.50'
}).lean();
const spoofedIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-proxy-6.6.6.6'
}).lean();
expect(clientIpRecord?.amount).toBe(1);
expect(spoofedIpRecord).toBeNull();
});
it('should use a shared fail-closed key when client IP cannot be resolved', async () => {
setTrustedProxyEnable(true);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-unknown',
seconds: 60,
limit: 10,
force: true
});
await middleware(
createReq({
headers: {
'x-forwarded-for': '203.0.113.50'
}
}),
createRes()
);
const unknownRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-unknown-unknown'
}).lean();
const spoofedIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-unknown-203.0.113.50'
}).lean();
expect(unknownRecord?.amount).toBe(1);
expect(spoofedIpRecord).toBeNull();
});
it('should block requests after the IP limit is exceeded', async () => {
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-block',
seconds: 60,
limit: 1,
force: true
});
const firstRes = createRes();
const secondRes = createRes();
const req = createReq({
remoteAddress: '198.51.100.30'
});
await middleware(req, firstRes);
await middleware(req, secondRes);
expect(jsonRes).toHaveBeenCalledTimes(1);
expect(jsonRes).toHaveBeenCalledWith(
secondRes,
expect.objectContaining({
code: 429
})
);
});
});
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import ipaddr from 'ipaddr.js';
import {
getClientIpFromRequest,
isTrustedProxyIp,
normalizeClientIp
} from '@fastgpt/service/common/security/clientIp';
import { serviceEnv } from '@fastgpt/service/env';
const originalTrustedProxyIps = serviceEnv.TRUSTED_PROXY_IPS;
const originalTrustedProxyEnable = serviceEnv.TRUSTED_PROXY_ENABLE;
const originalNodeEnv = process.env.NODE_ENV;
const setTrustedProxyEnable = (value: boolean) => {
serviceEnv.TRUSTED_PROXY_ENABLE = value;
};
const setTrustedProxyIps = (value?: string) => {
serviceEnv.TRUSTED_PROXY_IPS = value;
};
const setNodeEnv = (value?: string) => {
if (value === undefined) {
delete process.env.NODE_ENV;
return;
}
process.env.NODE_ENV = value;
};
const createReq = ({
headers = {},
remoteAddress
}: {
headers?: Record<string, string | string[]>;
remoteAddress?: string;
}) =>
({
headers,
socket: {
remoteAddress
}
}) as any;
describe('clientIp', () => {
afterEach(() => {
setTrustedProxyEnable(originalTrustedProxyEnable);
setTrustedProxyIps(originalTrustedProxyIps);
setNodeEnv(originalNodeEnv);
});
describe('normalizeClientIp', () => {
it('should normalize IPv4-mapped IPv6 and IP values with ports', () => {
expect(normalizeClientIp('::ffff:203.0.113.10')).toBe('203.0.113.10');
expect(normalizeClientIp('203.0.113.10:443')).toBe('203.0.113.10');
expect(normalizeClientIp('[2001:db8::10]:443')).toBe('2001:db8::10');
});
it('should ignore invalid IP values', () => {
expect(normalizeClientIp('not-an-ip')).toBeUndefined();
expect(normalizeClientIp('')).toBeUndefined();
});
it('should return undefined when ipaddr.process throws despite isValid passing', () => {
const processSpy = vi.spyOn(ipaddr, 'process').mockImplementation(() => {
throw new Error('boom');
});
try {
expect(normalizeClientIp('203.0.113.10')).toBeUndefined();
} finally {
processSpy.mockRestore();
}
});
});
describe('isTrustedProxyIp', () => {
beforeEach(() => {
setTrustedProxyEnable(true);
});
it('should not trust proxies when trusted proxy parsing is disabled', () => {
setTrustedProxyEnable(false);
setTrustedProxyIps('127.0.0.1/8, 10.0.0.0/8');
expect(isTrustedProxyIp('127.0.0.1')).toBe(false);
expect(isTrustedProxyIp('10.0.0.10')).toBe(false);
});
it('should trust loopback proxies by default', () => {
setTrustedProxyIps(undefined);
expect(isTrustedProxyIp('127.0.0.1')).toBe(true);
expect(isTrustedProxyIp('::1')).toBe(true);
expect(isTrustedProxyIp('10.0.0.1')).toBe(false);
});
it('should trust loopback proxies when NODE_ENV is unset on the first call', () => {
setNodeEnv(undefined);
setTrustedProxyIps(undefined);
expect(isTrustedProxyIp('127.0.0.1')).toBe(true);
});
it('should trust configured exact IP and CIDR ranges', () => {
setTrustedProxyIps('10.0.0.10, 172.16.0.0/12');
expect(isTrustedProxyIp('10.0.0.10')).toBe(true);
expect(isTrustedProxyIp('172.16.8.1')).toBe(true);
expect(isTrustedProxyIp('192.168.1.1')).toBe(false);
});
it('should ignore invalid and trust-all proxy settings', () => {
setTrustedProxyIps('not-an-ip, 0.0.0.0/0, 10.0.0.0/8/extra, 10.0.0.10');
expect(isTrustedProxyIp('203.0.113.50')).toBe(false);
expect(isTrustedProxyIp('10.0.0.20')).toBe(false);
expect(isTrustedProxyIp('10.0.0.10')).toBe(true);
});
it('should not trust loopback by default in production', () => {
setNodeEnv('production');
setTrustedProxyIps(undefined);
expect(isTrustedProxyIp('127.0.0.1')).toBe(false);
});
it('should warn once when TRUSTED_PROXY_IPS contains invalid entries outside test env', () => {
setNodeEnv('production');
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
setTrustedProxyIps('not-an-ip, 10.0.0.10');
// Trigger lazy compile of the trust function
expect(isTrustedProxyIp('10.0.0.10')).toBe(true);
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy.mock.calls[0]?.[0]).toContain('not-an-ip');
} finally {
warnSpy.mockRestore();
}
});
it('should trust explicitly configured loopback proxies in production', () => {
setNodeEnv('production');
setTrustedProxyIps('127.0.0.1/8, ::1/128');
expect(isTrustedProxyIp('127.0.0.1')).toBe(true);
expect(isTrustedProxyIp('::1')).toBe(true);
});
});
describe('getClientIpFromRequest', () => {
beforeEach(() => {
setTrustedProxyEnable(true);
});
it('should trust forwarding headers when trusted proxy parsing is disabled', () => {
setTrustedProxyEnable(false);
setTrustedProxyIps('127.0.0.1/8, 10.0.0.0/8');
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': '203.0.113.50',
'x-real-ip': '203.0.113.51'
}
})
);
expect(ip).toBe('203.0.113.50');
});
it('should fall back to X-Real-IP in compatibility mode when X-Forwarded-For is invalid', () => {
setTrustedProxyEnable(false);
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': 'not-an-ip',
'x-real-ip': '203.0.113.51'
}
})
);
expect(ip).toBe('203.0.113.51');
});
it('should use the left-most X-Forwarded-For IP in compatibility mode', () => {
setTrustedProxyEnable(false);
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': '203.0.113.50, 10.0.0.20'
}
})
);
expect(ip).toBe('203.0.113.50');
});
it('should ignore spoofed forwarding headers for direct requests', () => {
setTrustedProxyIps(undefined);
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '198.51.100.20',
headers: {
'x-forwarded-for': '203.0.113.50',
'x-real-ip': '203.0.113.51'
}
})
);
expect(ip).toBe('198.51.100.20');
});
it('should not trust private network proxies unless configured', () => {
setTrustedProxyIps(undefined);
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '10.0.0.20',
headers: {
'x-forwarded-for': '203.0.113.50'
}
})
);
expect(ip).toBe('10.0.0.20');
});
it('should read forwarding headers from a trusted loopback proxy', () => {
setTrustedProxyIps(undefined);
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': '203.0.113.50'
}
})
);
expect(ip).toBe('203.0.113.50');
});
it('should ignore loopback forwarding headers in production unless the proxy is configured', () => {
setNodeEnv('production');
setTrustedProxyIps(undefined);
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': '203.0.113.50'
}
})
);
expect(ip).toBe('127.0.0.1');
});
it('should read forwarding headers from explicitly configured loopback proxies in production', () => {
setNodeEnv('production');
setTrustedProxyIps('127.0.0.1/8, ::1/128');
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': '203.0.113.50'
}
})
);
expect(ip).toBe('203.0.113.50');
});
it('should use the right-most untrusted IP to resist spoofed X-Forwarded-For prefixes', () => {
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': '6.6.6.6, 203.0.113.50'
}
})
);
expect(ip).toBe('203.0.113.50');
});
it('should safely normalize repeated X-Forwarded-For headers before using proxy-addr', () => {
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': ['6.6.6.6', '203.0.113.50']
}
})
);
expect(ip).toBe('203.0.113.50');
});
it('should resolve forwarding headers case-insensitively', () => {
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'X-Forwarded-For': '203.0.113.50'
}
})
);
expect(ip).toBe('203.0.113.50');
});
it('should reject forwarding chains that contain invalid hops', () => {
expect(
getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': 'not-an-ip, 203.0.113.50'
}
})
)
).toBe('127.0.0.1');
expect(
getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': '203.0.113.50, '
}
})
)
).toBe('127.0.0.1');
});
it('should fall back to the direct peer when X-Forwarded-For is too long', () => {
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': '203.0.113.50,'.repeat(150)
}
})
);
expect(ip).toBe('127.0.0.1');
});
it('should fall back to the direct peer when X-Forwarded-For has too many hops', () => {
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': Array.from({ length: 33 }, () => '203.0.113.50').join(',')
}
})
);
expect(ip).toBe('127.0.0.1');
});
it('should peel configured trusted proxy hops from the forwarding chain', () => {
setTrustedProxyIps('10.0.0.0/8');
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '10.0.0.20',
headers: {
'x-forwarded-for': '198.51.100.10, 10.0.0.30'
}
})
);
expect(ip).toBe('198.51.100.10');
});
it('should fall back to the direct peer when the forwarded chain contains only trusted addresses', () => {
setTrustedProxyIps('10.0.0.0/8');
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '10.0.0.20',
headers: {
'x-forwarded-for': '10.0.0.30'
}
})
);
expect(ip).toBe('10.0.0.20');
});
it('should use X-Real-IP only when the direct peer is trusted', () => {
expect(
getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-real-ip': '203.0.113.60'
}
})
)
).toBe('203.0.113.60');
expect(
getClientIpFromRequest(
createReq({
remoteAddress: '198.51.100.20',
headers: {
'x-real-ip': '203.0.113.60'
}
})
)
).toBe('198.51.100.20');
});
it('should not use X-Real-IP when it points to a trusted proxy address', () => {
setTrustedProxyIps('10.0.0.0/8');
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '10.0.0.20',
headers: {
'x-real-ip': '10.0.0.30'
}
})
);
expect(ip).toBe('10.0.0.20');
});
it('should fall back to the trusted peer when forwarding header is invalid', () => {
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': 'not-an-ip'
}
})
);
expect(ip).toBe('127.0.0.1');
});
it('should return undefined when the request has no remote address', () => {
expect(
getClientIpFromRequest(
createReq({
headers: { 'x-forwarded-for': '203.0.113.50' }
})
)
).toBeUndefined();
});
it('should not fall back to X-Real-IP when X-Forwarded-For is present but invalid', () => {
const ip = getClientIpFromRequest(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': 'not-an-ip',
'x-real-ip': '203.0.113.60'
}
})
);
expect(ip).toBe('127.0.0.1');
});
});
});
declare global {
var countTrackQueue: Map<string, { event: string; count: number; data: Record<string, any> }>;
}
export {};
......@@ -60,6 +60,9 @@ catalogs:
'@types/node':
specifier: ^20
version: 20.17.24
'@types/proxy-addr':
specifier: 2.0.3
version: 2.0.3
'@types/request-ip':
specifier: ^0.0.38
version: 0.0.38
......@@ -76,8 +79,8 @@ catalogs:
specifier: ^3.9.5
version: 3.9.5
axios:
specifier: 1.15.2
version: 1.15.2
specifier: ^1.16.0
version: 1.16.0
chalk:
specifier: ^5.6.2
version: 5.6.2
......@@ -102,6 +105,9 @@ catalogs:
i18next:
specifier: 23.16.8
version: 23.16.8
ipaddr.js:
specifier: ^2.4.0
version: 2.4.0
js-yaml:
specifier: ^4.1.1
version: 4.1.1
......@@ -138,6 +144,9 @@ catalogs:
next-i18next:
specifier: 15.4.2
version: 15.4.2
proxy-addr:
specifier: 2.0.7
version: 2.0.7
proxy-agent:
specifier: ^6
version: 6.5.0
......@@ -220,7 +229,7 @@ importers:
version: 10.1.4(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.4)
next-i18next:
specifier: 'catalog:'
version: 15.4.2(i18next@23.16.8)(next@16.2.4(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 15.4.2(i18next@23.16.8)(next@16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
prettier:
specifier: 3.2.4
version: 3.2.4
......@@ -250,7 +259,7 @@ importers:
version: 0.6.1
axios:
specifier: 'catalog:'
version: 1.15.2
version: 1.16.0
cron-parser:
specifier: ^4.9.0
version: 4.9.0
......@@ -261,8 +270,8 @@ importers:
specifier: ^0.1.13
version: 0.1.13
ipaddr.js:
specifier: ^2.3.0
version: 2.3.0
specifier: 'catalog:'
version: 2.4.0
js-yaml:
specifier: 'catalog:'
version: 4.1.1
......@@ -353,7 +362,7 @@ importers:
version: 2.4.10
axios:
specifier: 'catalog:'
version: 1.15.2
version: 1.16.0
bullmq:
specifier: ^5.52.2
version: 5.52.2
......@@ -399,6 +408,9 @@ importers:
ioredis:
specifier: ^5.6.0
version: 5.6.0
ipaddr.js:
specifier: 'catalog:'
version: 2.4.0
joplin-turndown-plugin-gfm:
specifier: ^1.0.12
version: 1.0.12
......@@ -471,15 +483,15 @@ importers:
pino-opentelemetry-transport:
specifier: ^1.0.1
version: 1.0.1(@opentelemetry/api@1.9.0)(pino@9.7.0)
proxy-addr:
specifier: 'catalog:'
version: 2.0.7
proxy-agent:
specifier: 'catalog:'
version: 6.5.0
proxy-from-env:
specifier: ^1.1.0
version: 1.1.0
request-ip:
specifier: 'catalog:'
version: 3.3.0
tiktoken:
specifier: 1.0.17
version: 1.0.17
......@@ -526,12 +538,12 @@ importers:
'@types/pg':
specifier: ^8.6.6
version: 8.11.11
'@types/proxy-addr':
specifier: 'catalog:'
version: 2.0.3
'@types/proxy-from-env':
specifier: ^1.0.4
version: 1.0.4
'@types/request-ip':
specifier: 'catalog:'
version: 0.0.38
'@types/tunnel':
specifier: ^0.0.4
version: 0.0.4
......@@ -549,7 +561,7 @@ importers:
version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@chakra-ui/next-js':
specifier: 'catalog:'
version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.2.4(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)
version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)
'@chakra-ui/react':
specifier: 'catalog:'
version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
......@@ -606,7 +618,7 @@ importers:
version: 3.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
axios:
specifier: 'catalog:'
version: 1.15.2
version: 1.16.0
date-fns:
specifier: 'catalog:'
version: 3.6.0
......@@ -630,7 +642,7 @@ importers:
version: 16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)
next-i18next:
specifier: 'catalog:'
version: 15.4.2(i18next@23.16.8)(next@16.2.4(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 15.4.2(i18next@23.16.8)(next@16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
papaparse:
specifier: ^5.4.1
version: 5.4.1
......@@ -709,7 +721,7 @@ importers:
version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@chakra-ui/next-js':
specifier: 'catalog:'
version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.2.4(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)
version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)
'@chakra-ui/react':
specifier: 'catalog:'
version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
......@@ -751,7 +763,7 @@ importers:
version: 1.14.3
'@scalar/api-reference-react':
specifier: ^0.8.1
version: 0.8.1(axios@1.15.2)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)
version: 0.8.1(axios@1.16.0)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)
'@tanstack/react-query':
specifier: 'catalog:'
version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
......@@ -778,7 +790,7 @@ importers:
version: 10.5.0(postcss@8.5.6)
axios:
specifier: 'catalog:'
version: 1.15.2
version: 1.16.0
bufferutil:
specifier: ^4.1.0
version: 4.1.0
......@@ -841,7 +853,7 @@ importers:
version: 16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)
next-i18next:
specifier: 'catalog:'
version: 15.4.2(i18next@23.16.8)(next@16.2.4(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 15.4.2(i18next@23.16.8)(next@16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
nodemailer:
specifier: ^7.0.11
version: 7.0.13
......@@ -977,7 +989,7 @@ importers:
version: 5.1.0
axios:
specifier: 'catalog:'
version: 1.15.2
version: 1.16.0
chalk:
specifier: 'catalog:'
version: 5.6.2
......@@ -1020,7 +1032,7 @@ importers:
version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@chakra-ui/next-js':
specifier: 'catalog:'
version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.2.4(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)
version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)
'@chakra-ui/react':
specifier: 'catalog:'
version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
......@@ -1068,7 +1080,7 @@ importers:
version: 2.0.1
'@scalar/api-reference-react':
specifier: ^0.8.1
version: 0.8.1(axios@1.15.2)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)
version: 0.8.1(axios@1.16.0)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)
'@t3-oss/env-core':
specifier: 'catalog:'
version: 0.13.10(typescript@5.9.3)(zod@4.1.12)
......@@ -1083,7 +1095,7 @@ importers:
version: 7.0.1
axios:
specifier: 'catalog:'
version: 1.15.2
version: 1.16.0
date-fns:
specifier: 'catalog:'
version: 3.6.0
......@@ -1158,7 +1170,7 @@ importers:
version: 16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)
next-i18next:
specifier: 'catalog:'
version: 15.4.2(i18next@23.16.8)(next@16.2.4(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 15.4.2(i18next@23.16.8)(next@16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
nprogress:
specifier: ^0.2.0
version: 0.2.0
......@@ -1210,9 +1222,6 @@ importers:
remark-math:
specifier: ^6.0.0
version: 6.0.0
request-ip:
specifier: 'catalog:'
version: 3.3.0
sass:
specifier: ^1.58.3
version: 1.85.1
......@@ -1265,9 +1274,6 @@ importers:
'@types/react-syntax-highlighter':
specifier: ^15.5.6
version: 15.5.13
'@types/request-ip':
specifier: 'catalog:'
version: 0.0.38
'@typescript-eslint/eslint-plugin':
specifier: 'catalog:'
version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
......@@ -1315,7 +1321,7 @@ importers:
version: 8.3.4
axios:
specifier: 'catalog:'
version: 1.15.2
version: 1.16.0
crypto-js:
specifier: ^4.2.0
version: 4.2.0
......@@ -1376,7 +1382,7 @@ importers:
version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@chakra-ui/next-js':
specifier: 'catalog:'
version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.2.4(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)
version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)
'@chakra-ui/react':
specifier: 'catalog:'
version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
......@@ -1403,7 +1409,7 @@ importers:
version: 0.13.10(typescript@5.9.3)(zod@4.1.12)
axios:
specifier: 'catalog:'
version: 1.15.2
version: 1.16.0
i18next:
specifier: 'catalog:'
version: 23.16.8
......@@ -1415,7 +1421,7 @@ importers:
version: 16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)
next-i18next:
specifier: 'catalog:'
version: 15.4.2(i18next@23.16.8)(next@16.2.4(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 15.4.2(i18next@23.16.8)(next@16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
react:
specifier: ^18
version: 18.3.1
......@@ -5615,6 +5621,9 @@ packages:
'@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
'@types/proxy-addr@2.0.3':
resolution: {integrity: sha512-TgAHHO4tNG3HgLTUhB+hM4iwW6JUNeQHCLnF1DjaDA9c69PN+IasoFu2MYDhubFc+ZIw5c5t9DMtjvrD6R3Egg==}
'@types/proxy-from-env@1.0.4':
resolution: {integrity: sha512-TPR9/bCZAr3V1eHN4G3LD3OLicdJjqX1QRXWuNcCYgE66f/K8jO2ZRtHxI2D9MbnuUP6+qiKSS8eUHp6TFHGCw==}
......@@ -6312,8 +6321,8 @@ packages:
axios@1.13.6:
resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==}
axios@1.15.2:
resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==}
axios@1.16.0:
resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==}
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
......@@ -8061,6 +8070,15 @@ packages:
debug:
optional: true
follow-redirects@1.16.0:
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
......@@ -8707,8 +8725,8 @@ packages:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
ipaddr.js@2.3.0:
resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
ipaddr.js@2.4.0:
resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==}
engines: {node: '>= 10'}
is-absolute-url@4.0.1:
......@@ -14794,7 +14812,7 @@ snapshots:
'@chakra-ui/system': 2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1)
react: 18.3.1
'@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.2.4(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)':
'@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)':
dependencies:
'@chakra-ui/react': 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@emotion/cache': 11.14.0
......@@ -17009,7 +17027,7 @@ snapshots:
dependencies:
zod: 3.24.1
'@scalar/api-client@2.8.1(axios@1.15.2)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)':
'@scalar/api-client@2.8.1(axios@1.16.0)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)':
dependencies:
'@headlessui/tailwindcss': 0.2.2(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))
'@headlessui/vue': 1.7.23(vue@3.5.22(typescript@5.9.3))
......@@ -17034,7 +17052,7 @@ snapshots:
'@scalar/workspace-store': 0.17.1(typescript@5.9.3)
'@types/har-format': 1.2.16
'@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3))
'@vueuse/integrations': 13.9.0(axios@1.15.2)(focus-trap@7.6.5)(fuse.js@7.1.0)(nprogress@0.2.0)(qrcode@1.5.4)(vue@3.5.22(typescript@5.9.3))
'@vueuse/integrations': 13.9.0(axios@1.16.0)(focus-trap@7.6.5)(fuse.js@7.1.0)(nprogress@0.2.0)(qrcode@1.5.4)(vue@3.5.22(typescript@5.9.3))
focus-trap: 7.6.5
fuse.js: 7.1.0
js-base64: 3.7.8
......@@ -17065,9 +17083,9 @@ snapshots:
- typescript
- universal-cookie
'@scalar/api-reference-react@0.8.1(axios@1.15.2)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)':
'@scalar/api-reference-react@0.8.1(axios@1.16.0)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)':
dependencies:
'@scalar/api-reference': 1.38.1(axios@1.15.2)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)
'@scalar/api-reference': 1.38.1(axios@1.16.0)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)
'@scalar/types': 0.3.2
react: 18.3.1
transitivePeerDependencies:
......@@ -17086,11 +17104,11 @@ snapshots:
- typescript
- universal-cookie
'@scalar/api-reference@1.38.1(axios@1.15.2)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)':
'@scalar/api-reference@1.38.1(axios@1.16.0)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)':
dependencies:
'@floating-ui/vue': 1.1.9(vue@3.5.22(typescript@5.9.3))
'@headlessui/vue': 1.7.23(vue@3.5.22(typescript@5.9.3))
'@scalar/api-client': 2.8.1(axios@1.15.2)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)
'@scalar/api-client': 2.8.1(axios@1.16.0)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(typescript@5.9.3)
'@scalar/code-highlight': 0.2.0
'@scalar/components': 0.15.1(typescript@5.9.3)
'@scalar/helpers': 0.0.12
......@@ -18488,6 +18506,10 @@ snapshots:
'@types/prop-types@15.7.14': {}
'@types/proxy-addr@2.0.3':
dependencies:
'@types/node': 20.17.24
'@types/proxy-from-env@1.0.4':
dependencies:
'@types/node': 24.0.13
......@@ -18942,13 +18964,13 @@ snapshots:
'@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.9.3))
vue: 3.5.22(typescript@5.9.3)
'@vueuse/integrations@13.9.0(axios@1.15.2)(focus-trap@7.6.5)(fuse.js@7.1.0)(nprogress@0.2.0)(qrcode@1.5.4)(vue@3.5.22(typescript@5.9.3))':
'@vueuse/integrations@13.9.0(axios@1.16.0)(focus-trap@7.6.5)(fuse.js@7.1.0)(nprogress@0.2.0)(qrcode@1.5.4)(vue@3.5.22(typescript@5.9.3))':
dependencies:
'@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3))
'@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.9.3))
vue: 3.5.22(typescript@5.9.3)
optionalDependencies:
axios: 1.15.2
axios: 1.16.0
focus-trap: 7.6.5
fuse.js: 7.1.0
nprogress: 0.2.0
......@@ -19369,9 +19391,9 @@ snapshots:
transitivePeerDependencies:
- debug
axios@1.15.2:
axios@1.16.0:
dependencies:
follow-redirects: 1.15.11(debug@4.4.3)
follow-redirects: 1.16.0
form-data: 4.0.5
proxy-from-env: 2.1.0
transitivePeerDependencies:
......@@ -21425,6 +21447,8 @@ snapshots:
optionalDependencies:
debug: 4.4.3
follow-redirects@1.16.0: {}
for-each@0.3.5:
dependencies:
is-callable: 1.2.7
......@@ -22295,7 +22319,7 @@ snapshots:
ipaddr.js@1.9.1: {}
ipaddr.js@2.3.0: {}
ipaddr.js@2.4.0: {}
is-absolute-url@4.0.1: {}
......@@ -23722,7 +23746,7 @@ snapshots:
buffer-crc32: 1.0.0
eventemitter3: 5.0.1
fast-xml-parser: 5.5.8
ipaddr.js: 2.3.0
ipaddr.js: 2.4.0
lodash: 4.17.23
mime-types: 2.1.35
query-string: 7.1.3
......@@ -23944,7 +23968,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
next-i18next@15.4.2(i18next@23.16.8)(next@16.2.4(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
next-i18next@15.4.2(i18next@23.16.8)(next@16.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.10
'@types/hoist-non-react-statics': 3.3.6
......
......@@ -28,6 +28,7 @@ catalog:
'@types/react': ^18
'@types/react-dom': ^18
'@types/request-ip': ^0.0.38
'@types/proxy-addr': 2.0.3
'@typescript-eslint/eslint-plugin': ^6.21.0
'@typescript-eslint/parser': ^6.21.0
'@vitest/coverage-v8': ^4.1.5
......@@ -40,7 +41,8 @@ catalog:
'@chakra-ui/system': ^2
'@emotion/react': ^11
'@emotion/styled': ^11
axios: 1.15.2
axios: ^1.16.0
ipaddr.js: ^2.4.0
chalk: ^5.6.2
date-fns: ^3.6.0
dayjs: 1.11.19
......@@ -61,6 +63,7 @@ catalog:
nanoid: ^5.1.3
next: 16.2.4
next-i18next: 15.4.2
proxy-addr: 2.0.7
proxy-agent: ^6
react: ^18
react-dom: ^18
......
Subproject commit f269963dae71655c7eb82d8a5e20a9ecaf9d3cdd
Subproject commit a10974c0a73ca45f4e041738e5adb227923e289c
......@@ -10,7 +10,7 @@ SYNC_INDEX=true
# ==================== 密钥 ====================
# 用户账号密钥
TOKEN_KEY=fastgpt
# 文件阅读时的密钥,必须是 32 位以上的随机字符串
# 文件阅读时的密钥
FILE_TOKEN_KEY=
# 密钥加密 key
AES256_SECRET_KEY=fastgptsecret
......@@ -88,9 +88,9 @@ REDIS_URL=redis://default:mypassword@localhost:6379
# STREAM_RESUME_REDIS_MEMORY_CHECK_INTERVAL_MS=5000
# MongoDB 连接参数;本地开发连接远程数据库时,可能需要添加 directConnection=true 才能连接
MONGODB_URI="mongodb://myusername:mypassword@localhost:27017/fastgpt?authSource=admin&directConnection=true"
MONGODB_URI=mongodb://myusername:mypassword@localhost:27017/fastgpt?authSource=admin&directConnection=true
# 日志库
MONGODB_LOG_URI="mongodb://myusername:mypassword@localhost:27017/fastgpt?authSource=admin&directConnection=true"
MONGODB_LOG_URI=
# 向量库优先级: pg > oceanbase > milvus > opengauss
# 向量量化等级: PG 支持 32/16,OceanBase 支持 32/8/1
......@@ -140,6 +140,10 @@ FILE_DOMAIN=http://localhost:3000
USE_IP_LIMIT=false
# 启用内网 IP 检查
CHECK_INTERNAL_IP=false
# 是否启用可信反向代理客户端 IP 校验
TRUSTED_PROXY_ENABLE=false
# 可信反向代理 IP/CIDR 列表,逗号或空白分隔。仅 TRUSTED_PROXY_ENABLE=true 时生效;仅显式可信代理传入的 X-Forwarded-For/X-Real-IP 会用于客户端 IP 解析
TRUSTED_PROXY_IPS=
# 密码错误锁定时长(秒)
PASSWORD_LOGIN_LOCK_SECONDS=
# 密码过期月份(不设置则不过期)
......
......@@ -86,7 +86,6 @@
"remark-breaks": "^4.0.0",
"remark-gfm": "catalog:",
"remark-math": "^6.0.0",
"request-ip": "catalog:",
"sass": "^1.58.3",
"undici": "^7.18.2",
"use-context-selector": "^1.4.4",
......@@ -106,7 +105,6 @@
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/react-syntax-highlighter": "^15.5.6",
"@types/request-ip": "catalog:",
"@typescript-eslint/eslint-plugin": "catalog:",
"@typescript-eslint/parser": "catalog:",
"eslint": "catalog:",
......
import type { NextApiRequest, NextApiResponse } from 'next';
import { NextAPI } from '@/service/middleware/entry';
import { getCodeServerPasswordFromSandbox } from '@/service/core/sandbox/proxy';
import { normalizeClientIp } from '@fastgpt/service/common/security/clientIp';
// Internal-only endpoint: read the code-server password from the container config.yaml.
// Called by server.ts (running in the same process) to avoid importing service packages directly.
// Only requests from 127.0.0.1 are accepted.
async function handler(req: NextApiRequest, res: NextApiResponse) {
const clientIp = req.socket.remoteAddress;
const clientIp = normalizeClientIp(req.socket.remoteAddress);
if (clientIp !== '127.0.0.1' && clientIp !== '::1' && clientIp !== '::ffff:127.0.0.1') {
if (clientIp !== '127.0.0.1' && clientIp !== '::1') {
return res.status(403).json({ error: 'Internal only' });
}
......
......@@ -11,7 +11,6 @@ import { serviceEnv } from '@fastgpt/service/env';
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
import { authCode } from '@fastgpt/service/support/user/auth/controller';
import { createUserSession } from '@fastgpt/service/support/user/session';
import requestIp from 'request-ip';
import { setCookie } from '@fastgpt/service/support/permission/auth/common';
import { UserError } from '@fastgpt/global/common/error/utils';
import {
......@@ -20,6 +19,7 @@ import {
type LoginSuccessResponseType
} from '@fastgpt/global/openapi/support/user/account/login/api';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { getClientIpFromRequest } from '@fastgpt/service/common/security/clientIp';
async function handler(
req: ApiRequestProps<LoginByPasswordBodyType>,
......@@ -66,7 +66,7 @@ async function handler(
teamId: userDetail.team.teamId,
tmbId: userDetail.team.tmbId,
isRoot: username === 'root',
ip: requestIp.getClientIp(req)
ip: getClientIpFromRequest(req)
});
setCookie(res, token);
......
......@@ -7,6 +7,7 @@ import {
export async function register() {
try {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('@/env');
const { configureLogger, getLogger, LogCategories } = await import('@/service/logger');
await runInitializationStep({
step: 'configure-logger',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment