AI app builders make the same security mistakes again and again: database tables anyone can read, admin keys shipped to the browser, AI routes anyone can call. These checks look for them in your code.
They run in SafeWeave's cloud scans of your connected repositories on Cloud and Cloud Plus. Each finding shows the check's id, so you can find it here.
A Supabase write (.insert/.update/.delete/.upsert) is issued from client-reachable code. If the table's RLS policies do not scope writes to the current user, any visitor can modify data directly through the anon key. Confirm a restrictive WITH CHECK policy exists for this table.
Row Level Security is being DISABLED on a table. With RLS off, the anon and authenticated roles can read and write every row through the public API. Enable RLS and add policies instead.
ENABLE ROW LEVEL SECURITY;) and add explicit policies that scope access to the current user (e.g. USING (auth.uid() = user_id)). Never leave a user-data table with RLS off.
This RLS policy uses USING (true) or WITH CHECK (true), which allows every row through for anyone the polic…
Severity: High · Shown on findings as supabase-rls-passthrough-policy
This RLS policy uses USING (true) or WITH CHECK (true), which allows every row through for anyone the policy applies to. A "true" policy for anon or authenticated effectively disables Row Level Security for that operation.
How to fix: Replace the always-true policy with a real predicate that scopes rows to the caller, e.g. USING (auth.uid() = user_id) for SELECT/UPDATE/DELETE and WITH CHECK (auth.uid() = user_id) for INSERT/UPDATE. A blanket USING (true) is only appropriate for genuinely public lookup tables.
This Edge Function uses the service_role key (which bypasses Row Level Security) but never checks who is ca…
Severity: High · Shown on findings as supabase-edge-fn-service-key-no-auth
This Edge Function uses the service_role key (which bypasses Row Level Security) but never checks who is calling. Anyone who knows the function URL gets full database access through it.
How to fix: Before using the service_role client, create a client with the caller's Authorization header, call auth.getUser(), return 401/403 when there is no user or the user lacks the needed role (from app_metadata), and only then do the privileged work.
supabase.auth.admin is called from client code
Severity: High · Shown on findings as supabase-auth-admin-in-client
supabase.auth.admin is called from client code. Admin auth calls need the service_role key, which must never be in the browser.
How to fix: Move this auth.admin call into a server route or Edge Function that checks the caller is an admin, uses the service_role key from a server-only env var, and is called from the client with fetch.
Severity: Medium · Shown on findings as supabase-role-from-user-metadata
An authorization decision reads user_metadata. Users can change their own user_metadata with supabase.auth.updateUser, so they can make themselves admin.
How to fix: Store roles in app_metadata (only settable with the service_role key) or in a roles table protected by RLS, and read the role from there in both your server code and your RLS policies.
A SECURITY DEFINER function runs with its owner's rights but does not pin search_path, so a caller can shad…
Severity: Medium · Shown on findings as supabase-security-definer-no-search-path
A SECURITY DEFINER function runs with its owner's rights but does not pin search_path, so a caller can shadow tables or functions it uses.
How to fix: Add set search_path = '' to the function definition and fully qualify every table and function it uses (e.g. public.profiles). Also consider whether it needs SECURITY DEFINER at all.
A Supabase service_role key is exposed through a public env var (NEXT_PUBLIC_/VITE_)
Severity: High · Shown on findings as supabase-service-role-public-env
A Supabase service_role key is exposed through a public env var (NEXT_PUBLIC_/VITE_). Public env vars are inlined into the browser bundle, leaking a key that bypasses Row Level Security and grants full DB access.
How to fix: Never put the service_role key in a NEXT_PUBLIC_ or VITE_ env var. Rename it to a server-only var (e.g. SUPABASE_SERVICE_ROLE_KEY) and read it only from server code (Route Handler, Server Action, API route). The browser must use the anon key with Row Level Security.
The Supabase service_role key bypasses Row Level Security and grants full database access
Severity: High · Shown on findings as supabase-service-role-in-client
The Supabase service_role key bypasses Row Level Security and grants full database access. It must not be referenced in client-reachable code — keep it in server-only files (Route Handlers, Server Actions, API routes).
How to fix: Move the privileged Supabase client that uses the service_role key into a server-only file (a Route Handler like app/api/**/route.ts, a 'use server' action, or middleware). Client components should create the Supabase client with the anon key only.
A Supabase Storage bucket is created with public = true
Severity: Medium · Shown on findings as supabase-storage-public-bucket
A Supabase Storage bucket is created with public = true. A public bucket serves every object to anyone with the URL, with no authorization. If it holds user uploads, avatars, or documents, that data is world-readable.
How to fix: Make the bucket private (public = false) and serve objects through signed URLs (createSignedUrl) or a Storage RLS policy that checks the caller. Only keep a bucket public for genuinely public, non-sensitive assets.
Firebase
firebase-admin is imported in client code
Severity: High · Shown on findings as firebase-admin-in-client
firebase-admin is imported in client code. The Admin SDK needs a service account and bypasses all security rules; it must only run on a server.
How to fix: Move this Admin SDK call into a Cloud Function or server route (e.g. functions/src/index.ts with onCall), check the caller's req.auth there, and call it from the client with httpsCallable.
Admin access is decided from a Firestore document field in client code
Severity: Medium · Shown on findings as firebase-client-role-check
Admin access is decided from a Firestore document field in client code. Anyone who can edit that document, or the client, can grant themselves admin.
How to fix: Store roles as custom claims set by a Cloud Function (setCustomUserClaims), read them with user.getIdTokenResult() for UI only, and enforce them in security rules with request.auth.token.admin == true.
A Firebase/Google service-account key file is committed
Severity: High · Shown on findings as firebase-service-account-committed
A Firebase/Google service-account key file is committed. It grants admin access to your whole project and must never be in the repository.
How to fix: Delete this file from the repo and its git history, revoke the key in Google Cloud IAM, add the filename to .gitignore, and load credentials from the hosting environment (e.g. GOOGLE_APPLICATION_CREDENTIALS) instead.
These Firestore rules allow access with no condition
Severity: High · Shown on findings as firebase-firestore-open-rules
These Firestore rules allow access with no condition. Anyone with your app's public Firebase config can read or change this data.
How to fix: Replace the unconditional allow in firestore.rules with per-user rules, e.g. allow read, write: if request.auth != null && request.auth.uid == userId; on a /users/{userId} match. Never use if true or a bare allow read, write;.
These Cloud Storage rules allow access with no condition
Severity: High · Shown on findings as firebase-storage-open-rules
These Cloud Storage rules allow access with no condition. Anyone can download, overwrite or upload files in this bucket.
How to fix: In storage.rules, scope access to the owner, e.g. match /users/{uid}/{fileName} with allow read, write: if request.auth != null && request.auth.uid == uid;. Never use if true.
This Realtime Database rule is set to true, so anyone with your app's public config can read or write this…
Severity: High · Shown on findings as firebase-rtdb-open-rules
This Realtime Database rule is set to true, so anyone with your app's public config can read or write this path.
How to fix: In database.rules.json, replace ".read": true / ".write": true with conditions such as "auth != null && auth.uid == $uid" under a $uid path, so each user can only reach their own data.
Any signed-in user can write here
Severity: Medium · Shown on findings as firebase-auth-only-write
Any signed-in user can write here. Being logged in is not ownership: one user can edit or delete another user's documents.
How to fix: Require ownership as well as sign-in, e.g. allow write: if request.auth != null && request.auth.uid == resource.data.ownerId; and for creates check request.resource.data.ownerId == request.auth.uid.
Next.js
An entitlement / plan / subscription gate is being enforced in a client component ('use client')
Severity: Medium · Shown on findings as nextjs-client-side-entitlement-check
An entitlement / plan / subscription gate is being enforced in a client component ('use client'). Client-side checks are trivially bypassed — the user controls the browser. The same check must exist on the server.
How to fix: Move the entitlement check to the server: verify the user's plan / subscription in the Route Handler or Server Action that returns the gated data or performs the gated action. The client check can stay for UX, but it must not be the only gate.
A secret-looking value is exposed through a NEXT_PUBLIC_ variable
Severity: High · Shown on findings as nextjs-secret-in-public-env
A secret-looking value is exposed through a NEXT_PUBLIC_ variable. Next.js inlines NEXT_PUBLIC_ vars into the browser bundle, so anyone can read it.
How to fix: Rename the variable without the NEXT_PUBLIC_ prefix, read it only in server code (route handlers, server actions, server components), and rotate the exposed value.
Severity: Medium · Shown on findings as nextjs-dangerous-html-from-input
Request input (searchParams, params or query) is rendered with dangerouslySetInnerHTML without sanitizing, which allows cross-site scripting.
How to fix: Render the value as text ({value}) or sanitize it first with DOMPurify (DOMPurify.sanitize(value)) before passing it to dangerouslySetInnerHTML.
The app redirects to a URL taken straight from the query string
Severity: Medium · Shown on findings as nextjs-open-redirect
The app redirects to a URL taken straight from the query string. Attackers can send links that bounce your users to a phishing site.
How to fix: Only redirect to relative paths you validate (starts with "/" and not "//"), or to an allow-list of hosts; fall back to "/" otherwise.
A server route fetches a URL supplied by the caller
Severity: Medium · Shown on findings as nextjs-ssrf-fetch-user-url
A server route fetches a URL supplied by the caller. Attackers can make your server request internal addresses or cloud metadata endpoints (SSRF).
How to fix: Parse the URL and only fetch it when its hostname is in an explicit allow-list (and the protocol is https); reject everything else with 403.
This authorization check looks inverted: it appears to allow access when there is NO session/user, or to re…
Severity: High · Shown on findings as nextjs-inverted-auth-check
This authorization check looks inverted: it appears to allow access when there is NO session/user, or to reject when there IS one. An inverted check grants access to exactly the people it should block.
How to fix: Fix the inverted condition. Reject when there is NO session (if (!session) return 401), and proceed only when a valid session exists. Double-check the boolean: if (session) return 401 and if (!user) { ...proceed... } are the classic inversions.
This middleware matcher excludes /api
Severity: Medium · Shown on findings as nextjs-middleware-matcher-excludes-api
This middleware matcher excludes /api. If auth is enforced in middleware, excluding /api leaves API route handlers unprotected. Confirm each API route does its own auth, or don't exclude /api from the matcher.
How to fix: Either include /api in the middleware matcher so auth runs for API routes, or ensure every app/api/**/route.ts handler authenticates on its own. A matcher that skips /api while middleware is your only auth gate leaves the API open.
Severity: Medium · Shown on findings as nextjs-route-handler-no-auth
This Next.js Route Handler performs a data operation but does not call an auth check inside the handler. Unauthenticated callers may be able to read or write data. Authenticate at the top of the handler.
How to fix: At the top of this route handler, authenticate the caller before any data access — e.g. const session = await auth(); (or getServerSession / currentUser / supabase.auth.getUser) and return 401 if there is no session. Then scope the query to that user.
Severity: Medium · Shown on findings as nextjs-server-action-no-auth
A file with 'use server' actions performs a write (insert/update/delete) but does not call an auth check. Server Actions are callable by any client; a missing auth check lets anyone invoke the write.
How to fix: In each 'use server' action that writes data, authenticate first (const session = await auth(); if (!session) throw ...) and scope the write to the authenticated user. Server Actions are public endpoints — treat them like API routes.
Vite
A secret-looking value is exposed through a VITE_ variable
Severity: High · Shown on findings as vite-secret-in-public-env
A secret-looking value is exposed through a VITE_ variable. Vite inlines every VITE_ var into the JavaScript bundle, so anyone can read it.
How to fix: Remove the VITE_ prefix and move every use of this secret into a backend (server route, Supabase Edge Function or serverless function); rotate the exposed value. Only publishable keys (like the Supabase anon key) belong in VITE_ vars.
Paid content is hidden behind a flag checked only in the browser
Severity: Medium · Shown on findings as vite-client-only-paywall
Paid content is hidden behind a flag checked only in the browser. Anyone can flip the flag in devtools or call the data source directly.
How to fix: Serve paid data only from a server route (or RLS-protected table) that checks the user's subscription itself; the client check can stay for UI only.
An auth token is kept in localStorage in code that also writes innerHTML
Severity: Medium · Shown on findings as vite-token-in-localstorage-with-html-sink
An auth token is kept in localStorage in code that also writes innerHTML. Any XSS can read localStorage and steal the session.
How to fix: Keep the session in an httpOnly, secure cookie set by the server instead of localStorage, and replace innerHTML with textContent or sanitized rendering.
Admin access is decided by comparing the user's email to a hard-coded address in client code
Severity: Medium · Shown on findings as vite-hardcoded-admin-email
Admin access is decided by comparing the user's email to a hard-coded address in client code. The check runs in the browser and can be bypassed.
How to fix: Give admins a role stored server-side (app_metadata, custom claims or a roles table), enforce it on every admin API call or RLS policy, and use the client check only to show or hide links.
Express
CORS allows any origin together with credentials
Severity: High · Shown on findings as express-cors-any-origin-credentials
CORS allows any origin together with credentials. Any website can make logged-in requests to this API on behalf of your users.
How to fix: Set origin to an explicit list of your front-end origins (e.g. ['https://app.example.com']) when credentials: true is needed.
An /admin route goes straight to its handler with no auth middleware, so anyone can call it
Severity: Medium · Shown on findings as express-admin-route-no-auth
An /admin route goes straight to its handler with no auth middleware, so anyone can call it.
How to fix: Add auth middleware before the handler (e.g. app.get('/admin/users', requireAdmin, handler)) or mount all admin routes on a router that applies it with router.use(requireAdmin).
jwt.decode reads a token without checking its signature
Severity: High · Shown on findings as express-jwt-decode-for-auth
jwt.decode reads a token without checking its signature. Anyone can forge a token and become any user.
How to fix: Use jwt.verify(token, secret, { algorithms: ['HS256'] }) (or your actual algorithm) and reject the request when verification throws.
jwt.verify is called without an algorithms list, which leaves room for algorithm-confusion attacks
Severity: Medium · Shown on findings as express-jwt-verify-no-algorithms
jwt.verify is called without an algorithms list, which leaves room for algorithm-confusion attacks.
How to fix: Pass the expected algorithm explicitly: jwt.verify(token, secret, { algorithms: ['HS256'] }).
A cookie is set without httpOnly, so any script on the page (including an XSS payload) can read it
Severity: Medium · Shown on findings as express-cookie-not-httponly
A cookie is set without httpOnly, so any script on the page (including an XSS payload) can read it.
How to fix: Set session cookies with { httpOnly: true, secure: true, sameSite: 'lax' }.
Stripe
Severity: High · Shown on findings as stripe-entitlement-from-client-input
A subscription plan / entitlement is being set from client-supplied input (e.g. req.body.plan) rather than from the verified Stripe event. A user can send any plan value and grant themselves a paid tier for free.
How to fix: Derive the plan/entitlement from the Stripe object (the price/product id on the verified subscription or checkout event), never from the request body. Resolve req.body.plan to nothing; set the plan from event.data.object price/product mapping after signature verification.
stripe.webhooks.constructEvent is called with an empty or defaulted signing secret
Severity: High · Shown on findings as stripe-webhook-empty-secret
stripe.webhooks.constructEvent is called with an empty or defaulted signing secret. An empty secret makes signature verification meaningless — forged events will pass. Load the real webhook signing secret from the environment.
How to fix: Pass the actual webhook signing secret to constructEvent, read from a required env var (e.g. process.env.STRIPE_WEBHOOK_SECRET) that is set in production. Never default it to '' or "". Fail startup if it is missing.
This Stripe webhook handler reads the request body without calling stripe.webhooks.constructEvent to verify…
Severity: High · Shown on findings as stripe-webhook-no-signature-verification
This Stripe webhook handler reads the request body without calling stripe.webhooks.constructEvent to verify the signature. Anyone can POST a forged event to this endpoint and trigger fulfillment, plan changes, etc.
How to fix: Verify the webhook signature before trusting the event. Read the raw body and the 'stripe-signature' header, then call stripe.webhooks.constructEvent(rawBody, sig, WEBHOOK_SECRET) inside a try/catch and reject (400) on failure. Only act on the returned event.
AI apps
An LLM provider key is read from a public env var (NEXT_PUBLIC_, VITE_, EXPO_PUBLIC_, REACT_APP_)
Severity: High · Shown on findings as ai-llm-key-public-env
An LLM provider key is read from a public env var (NEXT_PUBLIC_, VITE_, EXPO_PUBLIC_, REACT_APP_). Public env vars are bundled into the browser, so anyone can copy the key and run up your AI bill.
How to fix: Rename the variable without the public prefix (e.g. OPENAI_API_KEY), rotate the exposed key, and call the model only from a server route that checks the user's session. The browser calls your route, never the provider.
The AI SDK client is created with dangerouslyAllowBrowser: true, which means the API key is shipped to the…
Severity: High · Shown on findings as ai-dangerously-allow-browser
The AI SDK client is created with dangerouslyAllowBrowser: true, which means the API key is shipped to the browser.
How to fix: Remove dangerouslyAllowBrowser and move this client into a server route (Next.js route handler, Express endpoint or edge function) that checks the user's session and applies a rate limit; have the browser call that route.
Severity: Medium · Shown on findings as ai-user-input-in-system-prompt
User input is placed inside the system prompt. Users can rewrite the assistant's instructions (prompt injection) and bypass its guardrails.
How to fix: Keep the system prompt a constant. Put user-supplied text in a separate user message, and validate any user choice (like a persona) against a fixed list before using it.
Model output flows straight into HTML, eval, a shell or raw SQL
Severity: High · Shown on findings as ai-output-to-dangerous-sink
Model output flows straight into HTML, eval, a shell or raw SQL. A prompt injection can turn the model's answer into XSS or code execution.
How to fix: Treat model output as untrusted input. Render it as text (textContent or a markdown renderer with sanitization), never eval it, and never build shell commands or SQL from it; map it to an allow-list of actions instead.
Severity: Medium · Shown on findings as ai-tool-shell-no-allowlist
An AI tool runs a shell command built from the model's arguments. A prompt injection can make the agent run any command on your server.
How to fix: Do not pass tool input to exec/spawn. Accept only an enum of named actions, map each to a fixed command run with execFile and fixed arguments, and reject anything else.
This API route calls an LLM but never checks who is calling
Severity: Medium · Shown on findings as ai-route-no-auth
This API route calls an LLM but never checks who is calling. Anyone can use it as a free AI proxy on your account.
How to fix: At the top of this handler, authenticate the caller (e.g. const session = await auth(); or supabase.auth.getUser()) and return 401 when there is no session, before calling the model.
This API route calls an LLM without a rate limit
Severity: Low · Shown on findings as ai-route-no-rate-limit
This API route calls an LLM without a rate limit. One user (or bot) can send thousands of requests and run up your AI bill.
How to fix: Before calling the model, apply a per-user rate limit (e.g. Upstash ratelimit.limit(userId) or express-rate-limit) and return 429 when the limit is reached. Also cap max tokens on the model call.
AI agents
A hardcoded token/API key appears in an MCP or editor config file (.mcp.json, claude_desktop_config.json, .…
Severity: High · Shown on findings as mcp-secret-in-config
A hardcoded token/API key appears in an MCP or editor config file (.mcp.json, claude_desktop_config.json, .cursor/mcp.json). These files are often committed and shared; secrets in them leak. Reference an env var.
How to fix: Move the secret out of the config file. Reference an environment variable (e.g. "env": { "API_KEY": "${API_KEY}" }) and set the real value in your shell/secret store. Remove the committed value and rotate it.
Severity: Medium · Shown on findings as mcp-tool-description-injection
An MCP tool description contains imperative instructions aimed at the model ("ignore previous...", "always call...", "send ... to..."). A malicious or careless description can hijack the agent (tool-description / prompt injection). Descriptions should describe the tool, not instruct the model.
How to fix: Rewrite the tool's description to plainly describe what it does and its parameters. Remove imperative directives to the model ("ignore", "always call", "you must", "send to"). If a description is user/config supplied, sanitize or reject such directives.
Severity: High · Shown on findings as mcp-tool-handler-shell-sink
An MCP tool handler passes its arguments into a shell / eval sink (child_process exec/spawn, eval, or Function). Tool arguments come from the model/caller; without strict validation this is command or code injection.
How to fix: Never pass tool arguments into a shell or eval. Use execFile/spawn with a fixed executable and an argument array (no shell:true, no string interpolation), or an allowlist of permitted commands/values. Drop eval and new Function entirely; parse the input instead.
Severity: Medium · Shown on findings as mcp-unbounded-fs-fetch-from-input
A tool handler reads a file or fetches a URL built directly from the caller's arguments, with no allowlist or path/host validation. An agent can be steered into reading arbitrary local files or making SSRF requests.
How to fix: Constrain the input before using it: resolve and check the path is within an allowed base directory (reject '..' and absolute paths), and for fetch, allowlist the host/scheme and block private/loopback addresses. Never pass raw tool arguments to readFile/fetch.
Secrets
An environment file appears to contain a real secret value (not a placeholder)
Severity: Medium · Shown on findings as secrets-real-value-in-env-file
An environment file appears to contain a real secret value (not a placeholder). If this .env file is committed, the secret is in your git history. .env files with real values must be gitignored, never committed.
How to fix: Ensure this file is in .gitignore and only commit a .env.example with placeholder values. If it was already committed, rotate the secrets and purge them from git history (git filter-repo / BFG). Real values belong in your deployment platform's secret store.
A hardcoded provider API key or token was found
Severity: High · Shown on findings as secrets-provider-key-hardcoded
A hardcoded provider API key or token was found. Live keys in source get committed, shared, and leaked. Move it to an environment variable and rotate the exposed value.
How to fix: Remove the hardcoded key. Read it from process.env at runtime, add the real value to your secret store / .env (gitignored), and ROTATE the key that was exposed — assume it is compromised once it has been in source.
Production browser source maps are enabled (productionBrowserSourceMaps: true)
Severity: Medium · Shown on findings as secrets-production-source-maps-enabled
Production browser source maps are enabled (productionBrowserSourceMaps: true). Shipping source maps publishes your original source — including comments and any inlined secrets — to anyone who opens devtools.
How to fix: Disable production source maps (remove productionBrowserSourceMaps: true in next.config, or devtool: false in the production webpack config). If you need them for error reporting, upload them privately to your monitoring tool and do not serve them from the public site.
Python
A shell command is executed with a dynamically built string via os.system, os.popen, or subprocess with she…
Severity: High · Shown on findings as py-command-injection-shell
A shell command is executed with a dynamically built string via os.system, os.popen, or subprocess with shell=True. If any part comes from user input, this is OS command injection (CWE-78).
How to fix: Avoid the shell. Call subprocess.run([...]) with an argument list and shell=False (the default) so arguments are passed directly to the program without shell interpretation. If you must build a command, validate against an allowlist and use shlex.quote — but a fixed executable + arg list is strongly preferred.
Flask is run with debug=True
Severity: High · Shown on findings as py-flask-debug-enabled
Flask is run with debug=True. The debug console exposes an interactive Python debugger (Werkzeug PIN can be brute-forced) and leaks stack traces — remote code execution risk if reachable in production (CWE-489).
How to fix: Never run with debug=True outside local development. Drive it from an environment variable that defaults to off (e.g. debug=os.environ.get("FLASK_DEBUG") == "1"), and serve production traffic with a WSGI server (gunicorn/uwsgi) rather than app.run.
Flask binds to 0.0.0.0 (all interfaces) while debug is enabled, exposing the debugger to the network
Severity: Medium · Shown on findings as py-flask-host-all-interfaces-with-debug
Flask binds to 0.0.0.0 (all interfaces) while debug is enabled, exposing the debugger to the network. Bind to localhost in development, or disable debug.
How to fix: Do not combine host="0.0.0.0" with debug=True. For local dev use the default host (127.0.0.1). If you need external access, disable debug and put a real WSGI server in front.
Severity: High · Shown on findings as py-sql-injection-string-built
A SQL query is built with string formatting (f-string, %, .format, or + concatenation) and passed to execute(). Interpolated values are not escaped, so any user-controlled input is SQL injection (CWE-89).
How to fix: Use a parameterized query: pass the SQL string with placeholders (%s or ?) as the first argument and the values as a separate tuple/list, e.g. cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)). Never build the query text from user input with f-strings, %, .format, or +.
Severity: Medium · Shown on findings as py-ssrf-request-to-user-url
An outbound HTTP request (requests/httpx/urllib) is made to a URL taken directly from request input, with no host allowlist or validation. An attacker can point it at internal services or cloud metadata (SSRF, CWE-918).
How to fix: Validate the destination before requesting it: parse the URL, allowlist the scheme (https) and the host against known-good domains, and reject private/loopback/link-local addresses (including the cloud metadata IP 169.254.169.254). Never pass a raw request-supplied URL to requests.get/httpx/urllib.
Untrusted data is deserialized with pickle, yaml.load (without SafeLoader), or marshal
Severity: High · Shown on findings as py-unsafe-deserialization
Untrusted data is deserialized with pickle, yaml.load (without SafeLoader), or marshal. These can execute arbitrary code during deserialization (CWE-502). Never deserialize data you did not produce yourself.
How to fix: Use a safe format for untrusted data: json.loads for JSON, or yaml.safe_load instead of yaml.load. Do not use pickle/marshal on data from the network, files, or users — if you control both ends and need pickle, sign and verify the payload out of band.
A broken/weak hash algorithm (MD5 or SHA-1) is used
Severity: Medium · Shown on findings as py-weak-hash-for-security
A broken/weak hash algorithm (MD5 or SHA-1) is used. These are unsuitable for passwords, tokens, or integrity where an attacker is a threat (CWE-327).
How to fix: For password storage use a slow, salted KDF (bcrypt, scrypt, or Argon2 via passlib/argon2-cffi), never a raw hash. For integrity/signatures use SHA-256 or better. If MD5/SHA-1 is genuinely non-security (e.g. a cache key), pass usedforsecurity=False to make intent explicit.