StingwardSecurity audit

Project security audit

my-saas

Serious problems found. Fix the critical and high items before you launch or bring in more users.

Target
./my-saas
Date
October 22, 2026
Type
Project
Findings
4 critical, 6 high, 7 medium, 1 low
Tool
Stingward 0.1.7

Generated by Stingward 0.1.7, open source (Apache 2.0). github.com/HiveControlAgents/StingWard

1Executive summary

GradeF

What the grade means

The grade goes from A to F and depends only on the most serious findings.

  1. AOnly low items, or none.
  2. BMedium items, nothing high or critical.
  3. COne or two high items, nothing critical.
  4. DThree or more high items, nothing critical.
  5. FAt least one critical item.

18 findings, grouped into 16 problems: the same problem in several places is listed once, with every place.

Fix these first

  1. Any visitor to your site can copy NEXT_PUBLIC_OPENAI_API_KEY and use it as you: run up the bill for that service or reach the data the key protects.

    Where:.env:1app/api/chat/route.ts:3

  2. A hidden instruction in any page, file or message the model reads becomes a command on your server or in your users' browsers.

    Where:app/api/agent/route.ts:16

  3. This version of next has a public, documented flaw: an attacker does not need to discover anything, only reuse a known attack. Probability CVE-2025-29927 gets exploited in the next 30 days: 99% (EPSS).

    Where:package-lock.json:15

2Remediation plan

Work from top to bottom. Effort is a rough estimate for someone who knows the code.

Quick: under an hour. Hours: fits in a day. Days: more than a day, usually because something has to be retested or rewritten.

#ProblemWhereEffort
Critical: fix now
1Secret exposed to the browser: NEXT_PUBLIC_OPENAI_API_KEY.env:1app/api/chat/route.ts:3Hours
2Text written by the model reaches a shell commandapp/api/agent/route.ts:16Hours
3next 14.1.0: 34 known vulnerabilities; worst is CVE-2025-29927 (Authorization Bypass in Next.js Middleware)package-lock.json:15Days
High: this week
4AI SDK running in the browser (dangerouslyAllowBrowser)app/api/chat/route.ts:3Hours
5Table profiles has no RLS in Supabasesupabase/migrations/0001_profiles.sql:1Hours
6SQL query built from request inputapp/api/invoices/route.ts:7Hours
7File path built from request input (fs.readFileSync())app/api/invoices/pdf/route.ts:6Hours
8Server fetches a URL taken from the request (fetch())app/api/preview/route.ts:4Hours
9axios 1.6.0: 31 known vulnerabilities; worst is CVE-2026-25639 (Axios is Vulnerable to Denial of Service via __proto__ Key in mergeConfig)package-lock.json:19Hours
Medium: this month
10.env is not in .gitignore.envQuick
11Supabase policy open to everyone: read invoices (select)supabase/migrations/0001_profiles.sql:9Hours
12CORS open to any websiteapp/api/chat/route.ts:8Quick
13Text written by the model reaches the page as HTMLcomponents/Chat.tsx:6Hours
14AI endpoint with no login and no rate limitapp/api/agent/route.ts:7app/api/chat/route.ts:5Hours
15requests >=2.19.0: the lowest version it allows (2.19.0) has 5 known vulnerabilities; worst is CVE-2018-18074 (Insufficiently Protected Credentials in Requests)worker/requirements.txt:2Hours
Low: when you can
16requirements.txt has no lockfile: 2 dependency(ies) with open versionsworker/requirements.txtQuick

3Findings in detail

Excerpts read from the files at audit time. Secrets are masked.

Application 8

#1CriticalRule APP001

Secret exposed to the browser: NEXT_PUBLIC_OPENAI_API_KEY

Where
  • .env:1
  • app/api/chat/route.ts:3
Evidence
.env line 1
1NEXT_PUBLIC_OPENAI_API_KEY=[REDACTED:secret_field]
app/api/chat/route.ts line 3
2
3const openai = new OpenAI({ apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY, dangerouslyAllowBrowser: true });
4
Why it matters

Variables prefixed with NEXT_PUBLIC_ are bundled into the JavaScript every visitor downloads.

How to fix

Drop the NEXT_PUBLIC_ prefix, use it server-side only (API route, server action, edge function) and rotate the key.

Standards
#2CriticalRule APP009

Text written by the model reaches a shell command

Where
  • app/api/agent/route.ts:16
Evidence
app/api/agent/route.ts line 16
15 });
16 exec(reply.choices[0].message.content ?? "");
17 return Response.json({ ok: true });
Why it matters

The model's answer can be steered by anything it read (a web page, a file, someone's message). Using it as is turns a prompt injection into commands running on your server.

How to fix

Treat model output like user input: map the answer to commands from a fixed list and pass arguments as a list, without a shell.

Standards
#4HighRule APP002

AI SDK running in the browser (dangerouslyAllowBrowser)

Where
  • app/api/chat/route.ts:3
Evidence
app/api/chat/route.ts line 3
2
3const openai = new OpenAI({ apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY, dangerouslyAllowBrowser: true });
4
Why it matters

Your OpenAI/Anthropic key ships to the browser. Someone copies it and runs up your bill.

How to fix

Call the AI API from a server route and keep the key there.

Standards
#5HighRule APP004

Table profiles has no RLS in Supabase

Where
  • supabase/migrations/0001_profiles.sql:1
Evidence
supabase/migrations/0001_profiles.sql line 1
1create table public.profiles (
2 id uuid primary key references auth.users,
Why it matters

Without Row Level Security anyone with the anon key (which is public) can read and change this table.

How to fix

Add: alter table profiles enable row level security; and create policies for each operation.

Standards
#11MediumRule APP005

Supabase policy open to everyone: read invoices (select)

Where
  • supabase/migrations/0001_profiles.sql:9
Evidence
supabase/migrations/0001_profiles.sql line 9
8alter table public.invoices enable row level security;
9create policy "read invoices" on public.invoices for select using (true);
Why it matters

The policy uses true as its condition, so it applies to anyone, signed in or not.

How to fix

Replace true with a condition such as auth.uid() = user_id.

Standards
#12MediumRule APP003

CORS open to any website

Where
  • app/api/chat/route.ts:8
Evidence
app/api/chat/route.ts line 8
7 const reply = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: prompt }] });
8 return new Response(JSON.stringify(reply), { headers: { "Access-Control-Allow-Origin": "*" } });
9}
Why it matters

Any page on the internet can call your API from a visitor's browser.

How to fix

Allow only your frontend domains in origin/allow_origins.

Standards
#13MediumRule APP009

Text written by the model reaches the page as HTML

Where
  • components/Chat.tsx:6
Evidence
components/Chat.tsx line 6
5 const { messages } = useChat();
6 return messages.map((m) => <div key={m.id} dangerouslySetInnerHTML={{ __html: m.content }} />);
7}
Why it matters

The model's answer can be steered by anything it read (a web page, a file, someone's message). Using it as is turns a prompt injection into scripts running in your users' browsers.

How to fix

Treat model output like user input: show it as text, or convert the Markdown and clean it with DOMPurify.sanitize before inserting it.

Standards
#14MediumRule APP011

AI endpoint with no login and no rate limit

Where
  • app/api/agent/route.ts:7
  • app/api/chat/route.ts:5
Evidence
app/api/agent/route.ts line 7
6// The user describes a chore in plain words and the agent does it on the server.
7export async function POST(req: Request) {
8 const { task } = await req.json();
app/api/chat/route.ts line 5
4
5export async function POST(req: Request) {
6 const { prompt } = await req.json();
Why it matters

Anyone who finds this URL can call the model on your account in a loop: the bill spikes and your app becomes a free AI proxy for others.

How to fix

Require a signed-in user, rate limit per user or IP (@upstash/ratelimit, express-rate-limit, slowapi), set max_tokens / maxOutputTokens, and add a spending limit in the provider's dashboard.

Standards

Dependencies 4

#3CriticalRule DEP001

next 14.1.0: 34 known vulnerabilities; worst is CVE-2025-29927 (Authorization Bypass in Next.js Middleware)

Where
  • package-lock.json:15
Evidence
package-lock.json line 15
14 },
15 "node_modules/next": {
16 "version": "14.1.0",
Why it matters

This version has public vulnerabilities. Others: CVE-2024-46982, CVE-2026-75604, GHSA-2xp9-vwfh-vxw4, CVE-2024-34351, CVE-2024-51479 and 28 more. Probability CVE-2025-29927 gets exploited in the next 30 days: 99% (EPSS).

How to fix

Upgrade next to 15.5.24 or later.

Known vulnerabilities in this version
IDSeveritySummaryFixed in
CVE-2025-29927CriticalAuthorization Bypass in Next.js MiddlewareEPSS 99%14.2.25
CVE-2024-46982CriticalNext.js Cache PoisoningEPSS 59%14.2.10
CVE-2026-75604CriticalNext.js: Unauthenticated Remote Code Execution on windows-hosted serversEPSS 2%15.5.24
GHSA-2xp9-vwfh-vxw4CriticalNext.js: Unauthenticated Remote Code Execution in Image Optimization API when AVIF file...15.5.24
CVE-2024-34351HighNext.js Server-Side Request Forgery in Server ActionsEPSS 5%14.1.1
CVE-2024-51479HighNext.js authorization bypass vulnerabilityEPSS 4%14.2.15
CVE-2026-44578HighNext.js vulnerable to server-side request forgery in applications using WebSocket upgradesEPSS 2%15.5.16
CVE-2026-64641HighNext.js: Denial of Service in App Router using Server Actions15.5.21

and 26 more

Standards
#9HighRule DEP001

axios 1.6.0: 31 known vulnerabilities; worst is CVE-2026-25639 (Axios is Vulnerable to Denial of Service via __proto__ Key in mergeConfig)

Where
  • package-lock.json:19
Evidence
package-lock.json line 19
18 },
19 "node_modules/axios": {
20 "version": "1.6.0",
Why it matters

This version has public vulnerabilities. Others: CVE-2024-39338, CVE-2025-58754, CVE-2026-44495, CVE-2026-44496, CVE-2026-44494 and 25 more.

How to fix

Upgrade axios to 1.20.0 or later.

Known vulnerabilities in this version
IDSeveritySummaryFixed in
CVE-2026-25639HighAxios is Vulnerable to Denial of Service via __proto__ Key in mergeConfigEPSS 2%1.13.5
CVE-2024-39338HighServer-Side Request Forgery in axiosEPSS 1%1.7.4
CVE-2025-58754HighAxios is vulnerable to DoS attack through lack of data size checkEPSS 1%1.12.0
CVE-2026-44495Highaxios Vulnerable to Credential Theft and Response Hijacking via Prototype Pollution Gad...EPSS 1%1.15.2
CVE-2026-44496HighAxios: Regular Expression Denial of Service (ReDoS) via Cookie Name Injection1.16.0
CVE-2026-44494Highaxios Vulnerable to Full Man-in-the-Middle via Prototype Pollution Gadget in `config.pr...1.16.0
CVE-2026-42033HighAxios: Prototype Pollution Gadgets - Response Tampering, Data Exfiltration, and Request...1.15.1
CVE-2025-27152Highaxios Requests Vulnerable To Possible SSRF and Credential Leakage via Absolute URL1.8.2

and 23 more

Standards
#15MediumRule DEP004

requests >=2.19.0: the lowest version it allows (2.19.0) has 5 known vulnerabilities; worst is CVE-2018-18074 (Insufficiently Protected Credentials in Requests)

Where
  • worker/requirements.txt:2
Evidence
worker/requirements.txt line 2
1openai>=1.30
2requests>=2.19.0
Why it matters

Without a lockfile the range is the only rule: a machine that already has version 2.19.0 keeps it, because it fits, and an install can pick it to satisfy other packages. Other issues in version 2.19.0: CVE-2023-32681, CVE-2024-47081, CVE-2024-35195, CVE-2026-25645.

How to fix

Raise the minimum to 2.33.0: replace requests>=2.19.0 with requests>=2.33.0. The next install then upgrades older copies.

Known vulnerabilities in the lowest version allowed
IDSeveritySummaryFixed in
CVE-2018-18074HighInsufficiently Protected Credentials in RequestsEPSS 7%2.20.0
CVE-2023-32681MediumUnintended leak of Proxy-Authorization header in requestsEPSS 3%2.31.0
CVE-2024-47081MediumRequests vulnerable to .netrc credentials leak via malicious URLs2.32.4
CVE-2024-35195MediumRequests `Session` object does not verify requests after making first request with veri...2.32.0
CVE-2026-25645MediumRequests has Insecure Temp File Reuse in its extract_zipped_paths() utility function2.33.0
Standards
#16LowRule DEP003

requirements.txt has no lockfile: 2 dependency(ies) with open versions

Where
  • worker/requirements.txt (the whole file)
Why it matters

Without a lockfile nobody knows which version gets installed, so it cannot be audited.

How to fix

Generate and commit a lockfile (npm install, poetry lock, uv lock) or pin versions with ==.

Standards

Code 3

#6HighRule COD001

SQL query built from request input

Where
  • app/api/invoices/route.ts:7
Evidence
app/api/invoices/route.ts line 7
6 const user = new URL(req.url).searchParams.get("user");
7 const { rows } = await pool.query(`select * from invoices where user_id = '${user}'`);
8 return Response.json(rows);
Why it matters

The value goes straight into the SQL text. Someone who sends ' OR '1'='1 can read, change or delete data.

How to fix

Pass values as parameters and let the driver combine them: cursor.execute("... WHERE id = %s", (id,)), $1 in Postgres, ? in SQLite, sql`...` or $queryRaw`...` in Prisma. A column or table name cannot be a parameter: pick it from a fixed list (SORTS[key], if order not in ALLOWED: abort(400)).

Standards
#7HighRule COD007

File path built from request input (fs.readFileSync())

Where
  • app/api/invoices/pdf/route.ts:6
Evidence
app/api/invoices/pdf/route.ts line 6
5 const file = new URL(req.url).searchParams.get("file") ?? "";
6 const pdf = fs.readFileSync(path.join(process.cwd(), "invoices", file));
7 return new Response(pdf, { headers: { "Content-Type": "application/pdf" } });
Why it matters

A value like ../../.env lets someone read or overwrite any file the server can reach.

How to fix

Keep only the file name (os.path.basename, secure_filename, path.basename), or resolve the path and check that it stays inside the folder you expect.

Standards
#8HighRule COD008

Server fetches a URL taken from the request (fetch())

Where
  • app/api/preview/route.ts:4
Evidence
app/api/preview/route.ts line 4
3 const { url } = await req.json();
4 const page = await fetch(url);
5 const html = await page.text();
Why it matters

Someone can make your server call internal addresses, such as the cloud metadata service (169.254.169.254), and get back the answer with credentials.

How to fix

Allow only the domains you need, block private and local addresses, and do not follow redirects without checking them.

Standards

Secrets 1

#10MediumRule SEC003

.env is not in .gitignore

Where
  • .env (the whole file)
Why it matters

It has not been committed yet, but the next git add . will take the keys along.

How to fix

Add .env to .gitignore.

Standards

4Scope and method

What was checked

Sources consulted

SourceUsed for Status
OSV.devknown vulnerabilities and malicious packagesconsulted
CISA KEVvulnerabilities attackers are exploiting nowconsulted, catalog 2026.10.21
EPSS (FIRST)likelihood of exploitation in the next 30 daysconsulted
NVDseverity scores missing from advisoriesnot needed: every advisory had a score

Only package names and versions left the machine, to these services. The code did not.

How it was done

  1. Listed the target's files, skipping dependency and build folders such as node_modules and dist. In a third-party skill the build output is read, since that is what runs.
  2. Read the lockfiles and asked OSV.dev about every package and version. Vulnerabilities on CISA's KEV list become critical; EPSS and NVD add the likelihood of exploitation and missing scores.
  3. Ran 50 rules for secrets, common AI-built app mistakes, flaws in the project's own code, agent and MCP settings, and skill content. Test folders, docs and examples count less.
  4. Graded the result from A to F by the most serious findings.

Limitations