Findings from an internal, pre-pentest code audit of the Infrawatch codebase. Each item is logged as a TODO for later triage, prioritisation and remediation. Nothing in this file has been fixed.
Scope covered:
- Authentication, session, identity, RBAC, licence
- Next.js HTTP API routes (
apps/web/app/api/*) - Server actions (
apps/web/lib/actions/*) - Go agent (
agent/), gRPC ingest (apps/ingest/), consumers - Database schema and query patterns (
apps/web/lib/db/) - Cryptography / secrets (
apps/web/lib/crypto/,apps/web/lib/licence.ts) - Deployment (Dockerfile, entrypoint, docker-compose, install.sh, start.sh,
next.config.ts)
Severity key: Critical / High / Medium / Low / Info.
Note: Findings were generated via static code review. Line numbers are approximate — verify by opening the file before remediation. Each claim should be confirmed during triage; some items may be false positives once runtime behaviour and middleware are considered.
-
[C-01] LDAP login can silently merge to an account in ANY organisation (cross-org takeover)
- Location:
apps/web/app/api/auth/ldap/route.ts:~67-75 - On LDAP bind success, the handler finds an existing user by email with no
organisationIdfilter, then links the LDAP account to whatever user row it finds. An attacker operating in Org A with control of an LDAP entry using a victim's email can hijack the Org B account when the LDAP user next authenticates. - Fix direction: scope
findFirstforusersby bothemailand the configured org id (eq(users.organisationId, config.organisationId)). Never link across organisations.
- Location:
-
[C-02] User management server actions trust a client-supplied
requesterId/invitedByIdwithout role verification- Location:
apps/web/lib/actions/users.ts—inviteUser(37),?),updateUserRole(deactivateUser,removeUser - The functions take the acting-user id as a parameter but never look it up to verify
role in ('org_admin', 'super_admin'). Any authenticated user can call the action directly (e.g., via the Next.js server-action RPC endpoint) and escalate, demote, or deactivate others. - Fix direction: replace parameter-supplied requester ids with
await auth.api.getSession()and enforce role at the start of every privileged action.
- Location:
-
[C-03] Email verification disabled by default in Better Auth
- Location:
apps/web/lib/auth/index.ts:~19(requireEmailVerification: false) - Anyone can register under any email (including executives of a target org), enabling phishing setup, squatting, and spoofed audit trails.
- Fix direction: require email verification; ship a verification-email flow before GA.
- Location:
-
[C-04] LDAP filter injection via unescaped
{{username}}substitution- Location:
apps/web/lib/ldap/client.ts:~307,apps/web/lib/actions/ldap.ts:~254 searchFilter = config.userSearchFilter.replace('{{username}}', username)does not escape LDAP metacharacters (*,(,),\, NUL) per RFC 4515. Attacker-crafted usernames (e.g.*))(|(uid=*) alter filter semantics, potentially bypassing auth or enumerating directory data.- Fix direction: apply RFC 4515 escaping (or
ldaptsbuilt-in escape) before substitution.
- Location:
-
[C-05] gRPC heartbeat handler falls back to client-supplied
AgentIdwhen JWT validation fails- Location:
apps/ingest/internal/handlers/heartbeat.go:~65-74 - When
ValidateAgentTokenerrors, the code setsagentID = first.AgentIdand continues. Anyone who knows a target agent id can heartbeat as that agent and push/read its data. - Fix direction: remove the fallback. Return
codes.Unauthenticatedon any JWT validation failure. Handle legacy clients via explicit version negotiation, not silent bypass.
- Location:
-
[C-06] Unauthenticated SSRF in certificate-checker tool
- Location:
apps/web/app/api/tools/certificate-checker/route.ts:~81-84(POST,action = fetch-url) - Endpoint has no session check and accepts arbitrary host+port, allowing unauthenticated callers to probe cloud metadata (
169.254.169.254), internal Kubernetes API, DBs, etc. Also no private-IP blocklist even once auth is added. - Fix direction: require authentication; add IP-range denylist (127.0.0.0/8, 10.0.0.0/8, 172.16/12, 192.168/16, 169.254/16, ::1, fc00::/7); restrict allowable ports.
- Location:
-
[C-07] Webhook/notification test-send is SSRF-capable (authenticated)
- Location:
apps/web/lib/actions/alerts.ts:~673-720(sendTestNotificationand channel creation) - Webhook/Slack/Telegram/SMTP fields accept arbitrary URLs/hostnames.
sendTestNotificationreaches them server-side with no SSRF guard, enabling internal-network probing and abuse from any authenticated account. - Fix direction: resolve target host and reject private/reserved IPs; force HTTPS for web hooks; cap size/timeout; rate-limit per org/user.
- Location:
-
[C-08] Missing authentication on most server-action read paths
- Location:
apps/web/lib/actions/*.ts— broad pattern. Examples includeagents.ts(listPendingAgents,listHosts,getHost,getHostMetrics, ...),alerts.ts(getAlertRules,getNotificationChannels, ...),certificates.ts(getCertificates,getCertificate,getCertificateCounts,deleteCertificate),checks.ts,domain-accounts.ts,host-groups.ts,host-settings.ts,notifications.ts,service-accounts.ts,software-inventory.ts,task-runs.ts. - Functions accept
orgIdas an argument and query the DB without verifying the caller is logged in, let alone a member of that org. Any unauthenticated request that can reach the Next.js action RPC endpoint can pull another org's data by guessing/enumerating org ids. - Fix direction: add
const session = await getRequiredSession(); if (session.user.organisationId !== orgId) throw ...to every action. Consider removingorgIdfrom public parameters and always deriving it from the session.
- Location:
-
[C-09]
deleteCertificate(and other destructive actions) have no auth/org check- Location:
apps/web/lib/actions/certificates.ts:~118-137 - Takes
orgId+certIdonly. An attacker can delete any certificate in any organisation. - Fix direction: session check + confirm the cert belongs to the session's org before deleting; use soft-delete (
deletedAt) per the universal table convention.
- Location:
-
[C-10] Agent self-update executes server-supplied binary with no signature verification
- Location:
agent/internal/updater/updater.go:~25-100 - Agent downloads and
execs a binary from a server URL without signature/hash verification. A compromised or MITM'd server pushes arbitrary code to every host as root. - Fix direction: sign release binaries; embed a pinned public key in the agent; verify signature + expected version before
exec. Consider TUF or cosign for release flow.
- Location:
-
[C-11] Hardcoded development public key used to validate production licence JWTs
- Location:
apps/web/lib/licence.ts - Resolved: the production public key (
PROD_PUBLIC_KEY_PEM) is now baked into the web image and used wheneverNODE_ENV=production. The dev key is only used in non-production. Customers verify infrawatch.io-issued licences against the embedded prod key with no configuration.
- Location:
-
[C-12] Raw SQL interpolation in
updateMetricRetention- Location:
apps/web/lib/actions/settings.ts:~80, ~90 sql.raw(String(days))is used inside a dynamically constructedINTERVALliteral. Although Zod constrainsdays,sql.rawbypasses parameterisation and represents a pattern that can be copied elsewhere less carefully. Any future change to the Zod schema (or a regression) turns this into SQL injection.- Fix direction: use Drizzle parameter binding (
sql\INTERVAL ${days} days``) and validate with strict integer coercion.
- Location:
-
[H-01] No rate limiting on authentication endpoints
apps/web/app/api/auth/[...all]/route.ts(Better Auth),apps/web/app/api/auth/ldap/route.ts- No per-IP or per-account throttling, no lockout, no CAPTCHA. Supports online brute-force of passwords, TOTP, invite tokens, LDAP bind creds.
- Fix: add rate-limiting middleware (e.g. upstash ratelimit, or the queue abstraction) keyed on IP + username; exponential backoff; generic "invalid credentials" response; lockout threshold.
-
[H-02] No CSRF protection on state-changing server actions or non-auth API routes
- Every server action under
apps/web/lib/actions/and most POST/PUT/DELETE routes underapps/web/app/api/lack explicit CSRF tokens and rely solely on cookies for auth. Next.js server actions are subject to CSRF unlesstrustedOriginsis strictly configured. - Fix: verify Better Auth
trustedOriginsis set to a tight allowlist; for API routes, validateOrigin/Referer; or implement the double-submit cookie pattern.
- Every server action under
-
[H-03] LDAP bind password encryption uses a hardcoded, shared salt
- Location:
apps/web/lib/crypto/encrypt.ts:~5-10(SALT = 'infrawatch-ldap-encryption-salt') scryptSync(secret, SALT, 32)produces the same key for every ciphertext, every config, every customer. IfBETTER_AUTH_SECRETleaks once, every stored LDAP password (across all orgs, installations) is decryptable.- Fix: random per-record salt persisted alongside ciphertext; separate dedicated encryption secret (not reused from auth).
- Location:
-
[H-04] LDAP bind password decryption uses
BETTER_AUTH_SECRETas KDF input- Location:
apps/web/lib/ldap/client.ts:~55-62 - Ties two unrelated trust domains together. Rotating
BETTER_AUTH_SECRETbreaks all stored LDAP passwords silently. - Fix: dedicated env var
LDAP_ENCRYPTION_KEY(or a secrets manager), with rotation tooling.
- Location:
-
[H-05] LDAP
tls_certificatecolumn stored in plaintext- Location:
apps/web/lib/db/schema/ldap-configurations.ts:~15-18 - Also
bind_dnis plaintext and may disclose internal directory structure. - Fix: encrypt sensitive LDAP columns at rest with the same mechanism used for
bind_password.
- Location:
-
[H-06] Weak password policy
apps/web/app/(auth)/register/register-form.tsx:~28,apps/web/lib/actions/profile.ts:~134- Minimum 8 chars, no complexity, no common-password check.
- Fix: raise minimum (≥12), integrate
zxcvbnor HIBP Pwned Passwords; document NIST-aligned rules.
-
[H-07] No account lockout / progressive delay after failed logins
- Custom LDAP endpoint and Better Auth login are both missing lockout.
- Fix: track consecutive failures per user + IP; lock account after N failures; notify the user by email.
-
[H-08] Invitation tokens generated with
createId()(cuid2), notrandomBytes- Location:
apps/web/lib/actions/auth.ts:~8-18+ invite issuance. - cuid2 is not designed as a security token; lacks the entropy guarantees of a cryptographically random 32-byte secret. No rate limit on invite lookup.
- Fix: generate tokens via
crypto.randomBytes(32).toString('hex'); rate-limitgetInviteByToken; short expiry; hash before storing.
- Location:
-
[H-09] Custom session cookie HMAC duplicated in LDAP handler
- Location:
apps/web/app/api/auth/ldap/route.ts:~109-125 - The handler re-implements cookie signing instead of delegating to Better Auth's signed cookie API. Divergence between the two implementations is a recurring source of session validation bypasses.
- Fix: remove custom HMAC; call Better Auth's signed-cookie helper (or create the session through Better Auth and redirect).
- Location:
-
[H-10] Terminal gRPC handler accepts expired JWTs and falls back to session_id
- Location:
apps/ingest/internal/handlers/terminal_grpc.go:~62-66 ValidateAgentTokenAllowExpired+agentID = "unknown"on error weakens the auth model. Attackers only need to guess/observe a session id.- Fix: require valid JWT; use the authenticated agent id to authorise the session, not a client-supplied session id.
- Location:
-
[H-11] Hostname/IP re-registration lets an attacker take over an existing host identity
- Location:
apps/ingest/internal/handlers/register.go:~40-238 - If an attacker knows a valid enrolment token and a target hostname, the collision-detection path can "adopt" the existing registration under a new keypair.
- Fix: require admin approval for any re-registration of a host with an existing record; alert on keypair change; pin public-key fingerprint on first registration.
- Location:
-
[H-12] Enrolment tokens are replay-safe only via usage counter (no hard max_uses/expiry)
- Location:
apps/ingest/internal/handlers/register.go:~55-65, ~150-192 - A leaked token can be used to register unlimited agents; no enforced expiry or max-uses.
- Fix: add
max_uses,expires_at; hash tokens at rest; audit-log every registration; rate-limit by source IP.
- Location:
-
[H-13]
/api/agent/bundle,/api/agent/install,/api/agent/download,/api/agent/latestlack rate limiting- Unauthenticated endpoints used for binary download and installer enrolment — trivial to abuse for DoS and recon.
- Fix: per-IP rate limits, size caps, and audit logging.
-
[H-14]
node-forgeX.509 parsing on untrusted input- Location:
apps/web/lib/certificates/fetch.ts:~87-89 - Parsing attacker-controlled certificates (from remote hosts via
/api/tools/certificate-checker, from uploads) exposes a historically bug-prone ASN.1 parser. Combined with C-06 this is a direct reachability path. - Fix: prefer Node's built-in
crypto.X509Certificate; keep node-forge pinned and patched; cap certificate size; wrap in a resource-limited worker.
- Location:
-
[H-15]
lib/certificates/fetch.tshas no private-IP / SSRF denylistfetchCertPemsFromUrlconnects to any hostname with a 10s timeout. Called fromtrackCertificateFromUrland from the cert-checker endpoint.- Fix: resolve DNS first; reject private ranges and
169.254/16; allow opt-in for specific internal hostnames only.
-
[H-16] Secrets leaked in notification channel create/update response
- Location:
apps/web/lib/actions/alerts.ts:~517-671 createNotificationChannelreturns the raw config object (including webhook auth headers, SMTP password, Telegram token) back to the caller.getNotificationChannelssanitises, but create/update does not.- Fix: always return the sanitised
NotificationChannelSafeshape.
- Location:
-
[H-17] Potential SSH private-key exposure in
getServiceAccount- Location:
apps/web/lib/actions/service-accounts.ts:~144-150 - Function returns
sshKeysrows directly — need to verify the schema never includes private-key material before returning to the client. - Fix: explicitly project only public fields; never return private-key columns from any action.
- Location:
-
[H-18] Dockerfile
entrypoint.shruns as root before dropping tonextjs- Location:
apps/web/Dockerfile:~92-103+apps/web/entrypoint.sh chown -R nextjs:nodejs /var/lib/infrawatch/agent-distruns as root at every boot. If the script or the directory is writable by thenextjsuser, a privilege-escalation primitive exists.- Fix: pre-create directories with correct ownership at build time; drop root entirely; make the volume owned by
nextjsvia--chownmount options.
- Location:
-
[H-19] Docker images referenced by tag, not digest
- Location:
docker-compose.single.yml:~3-4, ~20, ~44 - Tag reassignment (compromised GHCR, or supply chain) replaces running code silently. Comment already acknowledges pinning should be done.
- Fix: pin
image: ...@sha256:...in every profile used for production; update via CI.
- Location:
-
[H-20]
install.shis a curl-piped-to-bash installer with no signature/checksum verification- Location:
install.sh:~1-10 - Downloads and unpacks a ZIP from GitHub releases with no integrity check. A release compromise (or MITM for anyone without TLS pinning) distributes backdoors to all operators.
- Fix: publish GPG or cosign signatures; verify SHA256 checksum against a known value; prefer container images.
- Location:
-
[H-21]
direct_access = trueterminal mode grants PTY as root with no extra checks- Location:
agent/internal/terminal/session.go:~82-116 - Direct-access mode bypasses the per-user
sudrop. - Fix: require org_admin + explicit per-host opt-in + audit log; prefer per-user sessions by default; require MFA for direct-access sessions.
- Location:
-
[H-22] Task script execution has no sandbox / resource limits
- Location:
agent/internal/tasks/script.go:~33-111 - Server-supplied script body is written to a temp file and executed as the agent user (typically root) with no cgroup, seccomp, or timeout limit. If the server or server-side authorisation is compromised, this is direct RCE on every host.
- Fix: execute scripts under an unprivileged user by default; enforce CPU/memory/time limits; log every script to an append-only audit store; require task payload to be signed server-side with a key the agent validates.
- Location:
-
[H-23] gRPC ingest server missing
MaxRecvMsgSize,MaxSendMsgSize,MaxConcurrentStreams- Location:
apps/ingest/internal/grpc/server.go:~49-100 - A malicious or buggy agent can push huge messages or open unlimited streams until the server OOMs.
- Fix: set conservative caps (e.g. 50 MB message, 1k streams per connection); add keepalive and stream deadlines.
- Location:
-
[H-24] Missing composite index on
host_metrics(organisation_id, host_id, recorded_at)- Location:
apps/web/lib/db/schema/metrics.ts:~6-24 - Time-series queries with tenant filters fall back to full table scans on a hypertable that will hold the bulk of all data; any user can degrade the entire cluster by requesting wide time ranges.
- Fix: add the composite index (or TimescaleDB chunk-aware equivalent); cap the time range allowed in user-facing queries.
- Location:
-
[H-25] Invite/password-reset/invitation lookups miss rate limits
getInviteByToken,acceptInvite, LDAP bind, email/password login.- Fix: uniform rate-limiting middleware across auth-adjacent actions.
-
[H-26]
proxy.tsand any reverse-proxy behaviour may be abusable for SSRF- Location:
apps/web/proxy.ts— needs review against the agent/ingest traffic path to ensure it does not forward attacker-controlled URLs to arbitrary internal services. - Fix direction: triage during hardening; allowlist target hosts; strip hop-by-hop headers.
- Location:
-
[H-27] Missing security headers / CSP
- Location:
apps/web/next.config.ts:~1-12 - No CSP,
X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy. Clickjacking, MIME sniffing, and referrer-based token leakage are all possible. - Fix: add
headers()returning a strict header set; consider middleware for nonces to enable strict CSP.
- Location:
-
[H-28]
BETTER_AUTH_SECRETandBETTER_AUTH_URLfall back to insecure defaults- Location:
apps/web/lib/auth/index.ts:~30-32 - Empty secret + localhost URL are accepted at startup; failures surface only when a crypto operation runs.
- Fix: fail-fast validation at boot for all required env vars; refuse to start with empty/placeholder values; provide a
./scripts/doctorcommand.
- Location:
-
[H-29] LIKE searches built with unescaped user input
- Files:
apps/web/lib/actions/domain-accounts.ts:~63,service-accounts.ts:~66,certificates.ts:~53,software-inventory.ts:~240, ~312 %and_wildcards are not escaped. Not a classic SQL-injection but produces unbounded scans (DoS) and bypasses the caller's apparent filter.- Fix: reuse
escapeLikePattern(already present insoftware-inventory.ts) across all modules; add linting.
- Files:
-
[H-30] Hard delete for
domain_accountsbreaks the soft-delete invariant- Location:
apps/web/lib/actions/domain-accounts.ts:~213-215 - CLAUDE.md states every table uses
deleted_at. Hard delete silently loses audit history and breaks cascade assumptions (e.g., event spine references). - Fix: soft-delete via
deletedAt; revisit any otherdb.delete(...)occurrences.
- Location:
-
[H-31] Default Postgres credentials in
docker-compose.single.yml- Location:
docker-compose.single.yml:~6-8, ~31(infrawatch:infrawatch@db:5432/infrawatch) - Operators who skip editing
.envwill ship with trivially guessable credentials. - Fix: require
POSTGRES_PASSWORDto be set (no default); generate a random password on first run; document the requirement prominently.
- Location:
-
[M-01] User enumeration via differential responses / timing on the LDAP endpoint
apps/web/app/api/auth/ldap/route.ts:~28-29, ~148— generic error externally but detailed logs internally; response time may differ.- Fix: constant-time comparisons; artificial jitter; identical error strings.
-
[M-02] Stack traces / raw error strings returned to clients
- Examples:
apps/web/app/api/auth/ldap/route.ts:~152,apps/web/app/api/tools/certificate-checker/route.ts:~138-141, manycatch (err) { return { error: err.message } }inlib/actions/*. - Fix: centralised error handler; log details server-side; return stable error codes / generic strings.
- Examples:
-
[M-03]
licencevalidation allows unsigned algorithm?- Location:
apps/web/lib/licence.ts - Triage required — confirm the JWT verification call enforces
algorithms: ['RS256']and rejectsalg=none. - Fix: explicitly allowlist algorithm; assert
typ,iss,aud,exp,iat.
- Location:
-
[M-04] Agent config file permissions not verified on load
- Location:
agent/internal/install/install.go:~104, ~126, ~190, ~230;agent/internal/config/config.go - Written 0600 on install but a later
chmodor user modification goes undetected. - Fix: on boot,
Statconfig files; refuse to start if mode is > 0600 or ownership is non-root.
- Location:
-
[M-05] Certificate file checks follow symlinks
- Location:
agent/internal/checks/cert_file.go:~96-112 os.ReadFilefollows symlinks by default; an attacker with local write access to a monitored directory can point the check at arbitrary files to exfiltrate or confuse alerts.- Fix:
os.Lstatand reject symlinks; or useos.OpenFilewithO_NOFOLLOW.
- Location:
-
[M-06] Terminal WS session IDs accepted from URL path
- Location:
apps/ingest/internal/handlers/terminal_ws.go:~46-53 - Needs verification that session ids are cryptographically random (≥128 bits) and single-use.
- Fix: generate with
crypto/rand; bind to the authenticated user; expire on disconnect.
- Location:
-
[M-07] Terminal username regex is too permissive
- Location:
agent/internal/terminal/session.go:~91-94andapps/web/lib/actions/terminal.ts:~87-99 [a-zA-Z0-9._@\-]+allows@,.and-leading characters that some shells orsuimplementations treat specially.- Fix: defer to
user.Lookup; do not do shell-style interpolation on the username.
- Location:
-
[M-08] JSONB metadata columns have no runtime validation
- Location: every table with
metadata: jsonb('metadata')(hosts, alerts, events, etc.) .$type<T>()provides compile-time typing only; runtime code may consume unexpected fields, and any code path that evaluates metadata (templating, handlebars, etc.) is a potential code-exec risk.- Fix: Zod-parse metadata on read/write boundaries; do not
evalor template it without sanitisation.
- Location: every table with
-
[M-09] No audit log for sensitive mutations
- Role changes, user removal, alert-rule deletion, silence creation, host deletion, terminal-settings changes, enrolment-token creation, licence updates, notification-channel modification.
- Fix: introduce append-only
audit_eventstable; write entries from a single helper invoked by every privileged action.
-
[M-10] Missing rate limiting on expensive actions
alerts.sendTestNotification,software-inventory.triggerSoftwareScan,software-inventory.getSoftwareReport,certificates.trackCertificateFromUrl,agents.createEnrolmentToken.- Fix: per-org token-bucket or leaky-bucket limiter.
-
[M-11] Cert-file path validation too permissive
- Location:
apps/web/lib/actions/checks.ts:~35-40(certFileConfigSchema.filePath) - Only
.min(1). Allows/etc/shadow,../../etc/passwd,/proc/*/mem. Defence-in-depth; the agent must still restrict, but the server should not accept such paths. - Fix: explicit allowlist/regex; reject
.., absolute paths outside an allowed prefix, and/proc,/sys,/dev.
- Location:
-
[M-12] Notification channel configuration (SMTP, webhook) accepts plain HTTP
apps/web/lib/actions/alerts.ts— verify URL schema validation, TLS enforcement, and port allowlist.- Fix: require HTTPS (unless explicitly overridden per org); restrict SMTP ports and TLS modes.
-
[M-13] Licence JWT bound only to offline validation; no revocation signal
- Location:
apps/web/lib/licence.ts - A leaked/forged licence cannot be revoked. Combined with C-11 this is more urgent.
- Fix: add revocation list distributed via signed bundle; shorter
exp; periodic server-side re-validation when online.
- Location:
-
[M-14]
$$ECB$$/MD5/SHA1usage for security-relevant purposes?- Triage: confirm no uses of MD5 or SHA1 in auth or integrity paths.
fetch.tsuses SHA1 only for display fingerprints (Low). - Fix: explicit deny-list in CI (lint for
createHash('md5'|'sha1'),createCipheriv('*-ecb'|'*-cbc'without HMAC)).
- Triage: confirm no uses of MD5 or SHA1 in auth or integrity paths.
-
[M-15] Encryption format in
lib/crypto/encrypt.tsis:separated- Location:
apps/web/lib/crypto/encrypt.ts:~23, ~28-33 - String splitting is safe today, but packed binary (
iv||tag||ciphertextbase64) is less fragile as the format evolves. - Fix: canonical binary layout + length prefixes.
- Location:
-
[M-16] Agent heartbeat buffers unbounded task progress / result bytes
- Location:
agent/internal/heartbeat/heartbeat.go:~115, ~395-406 - Large task output builds up in memory before a heartbeat delivers it.
- Fix: cap buffered output per task and per interval; truncate with explicit "output truncated" marker; stream large outputs directly to server.
- Location:
-
[M-17]
getActiveEnrolmentTokenandlistEnrolmentTokensreturn plaintext tokens- Location:
apps/web/lib/actions/agents.ts— triage whether tokens are stored hashed or in plaintext and what is returned inGETresponses. - Fix: store tokens hashed (argon2id or SHA-256 with random secret), display only once on creation, show hints (last 4 chars) afterwards.
- Location:
-
[M-18]
INGEST_WS_URLreturned to the client- Location:
apps/web/lib/actions/terminal.ts:~115 - Exposes internal topology; in many deployments the ingest URL should not be browser-reachable.
- Fix: proxy WS through Next.js or a same-origin hostname; do not emit raw
process.envto clients.
- Location:
-
[M-19] Cert fetch has no response-size limit
- Location:
apps/web/lib/certificates/fetch.ts - A malicious server can return arbitrarily large data; parsing a huge "cert" causes memory pressure.
- Fix: cap read bytes (e.g. 64 KB per cert); enforce TLS handshake timeout separately from read timeout.
- Location:
-
[M-20] No request-id / correlation-id on API responses
- Hampers forensic tracing during incident response.
- Fix: middleware injecting
X-Request-Id; include it in error responses and logs.
-
[M-21] Host deletion cascade has TOCTOU between existence check and transaction
- Location:
apps/web/lib/actions/agents.ts:~634-814 - Concurrent requests can race across the cascade.
- Fix: move the lookup inside the transaction with
for update; or rely on FK cascades where possible.
- Location:
-
[M-22]
createGroup/updateGroupaccept untypeddatawithout Zod- Location:
apps/web/lib/actions/host-groups.ts:~14-34 - Fix: add explicit schemas.
- Location:
-
[M-23] CSV export formula-injection mitigation is incomplete
- Location:
apps/web/app/api/reports/software/export/route.ts:~44-52 - Single-quote prefix is defeated by some spreadsheet programs.
- Fix: also escape with a tab, document the risk, or export as XLSX/PDF only.
- Location:
-
[M-24] Software export query has 250k row limit but no query-level timeout
- Location:
apps/web/app/api/reports/software/export/route.ts:~112-129 - A complex filter can still run for minutes, holding a DB connection.
- Fix: set
statement_timeoutper transaction (e.g. 30s).
- Location:
-
[M-25]
/api/agent/latestis unauthenticated and calls GitHub- Location:
apps/web/app/api/agent/latest/route.ts - No auth + outbound network call — DoS amplifier and infrastructure fingerprinting.
- Fix: cache response; rate-limit; optionally require authentication for the management UI copy.
- Location:
-
[M-26] PDF generation library surface (
apps/web/lib/pdf/)- Triage: confirm the PDF generator cannot be induced to fetch attacker-controlled URLs (SSRF), embed attacker-supplied HTML (XSS in PDF), or load local files. Review template rendering pipeline.
- Fix direction: disable JavaScript and remote resource loading in the PDF engine; sanitise inputs; run inside a locked-down subprocess.
-
[M-27] Certificate import / upload does not validate size and type server-side
apps/web/lib/actions/certificates.ts—trackCertificateFromUploadmust bound size and reject anything that isn't a parseable PEM/DER.- Fix:
zodrefinecheck + explicit byte-length limit.
-
[M-28] Missing
Originvalidation on Next.js server actions- Server actions accept POST from any origin unless trustedOrigins is tight.
- Fix: configure
trustedOriginsstrictly; add middleware that rejects unknown origins.
-
[M-29]
/api/agent/bundleautoApprove tokens bypass registration approval- Location:
apps/web/app/api/agent/bundle/route.ts:~26-31 - Compromise of an org_admin account = silent agent enrolment at scale.
- Fix: require separate dual-approval for
autoApprovetokens; audit-log + email notification on generation.
- Location:
-
[M-30] Inventory submission accepts arbitrarily large chunks
- Location:
apps/ingest/internal/handlers/inventory.go:~98-137 - No enforced per-chunk max size.
- Fix: reject chunks > N packages; bail out early.
- Location:
-
[M-31] Terminal session ingest WS URL derived from env; no TLS verify enforced in all paths
- Triage across agent, ingest, and web for any
InsecureSkipVerify: trueuse. Cert refresh sweeper (apps/ingest/internal/handlers/cert_refresh_sweeper.go:~185-188) intentionally uses it; confirm no other paths do. - Fix: document + restrict
InsecureSkipVerifyto the cert-refresh sweeper; require explicit flag to enable elsewhere.
- Triage across agent, ingest, and web for any
-
[M-32] LDAP injection defence also missing in
apps/web/lib/actions/ldap.ts:~254- Same root cause as C-04; separate location to be patched.
- Fix: centralise a single LDAP-escape helper and use it everywhere.
-
[L-01] SHA1 fingerprints shown in UI/API alongside SHA256
apps/web/lib/certificates/fetch.ts:~100-102. No security impact for display, but reduces clarity and invites future misuse.- Fix: drop SHA1 or hide behind an "advanced" toggle.
-
[L-02] Dev TLS certs generated with 10-year expiry
start.sh:~113-118. If copied into production, cert rotation is skipped for a decade.- Fix: 90–365 day dev certs; document strict cert rotation for production.
-
[L-03]
.env.examplecontains only localhost defaults (safe) but lacks explicit comments on mandatory production values- Fix: add comments marking which variables are security-critical; add boot-time validation (see [H-28]).
-
[L-04] Logs may leak sensitive fields if callers
console.logthe whole row- Multiple server actions and handlers log raw
err/ config. - Fix: structured logger with automatic redaction for known sensitive keys (
password,token,bindPassword,config).
- Multiple server actions and handlers log raw
-
[L-05] Non-constant-time string comparisons for tokens/fingerprints
- Triage for
===comparisons of secrets acrossapps/weband Go code. - Fix:
timingSafeEqual/subtle.ConstantTimeCompare.
- Triage for
-
[L-06] Task-runs custom scripts accept any body size
apps/web/lib/actions/task-runs.ts:~284-327- Fix: enforce max length per interpreter; optional syntax linting.
-
[L-07] Inconsistent role constants
networks.ts,notification-settings.ts,terminal.ts,software-inventory.tseach redefineADMIN_ROLES.- Fix: centralise in
lib/auth/roles.ts; export typed constants.
-
[L-08]
idspread overparsed.datain create functions- Examples:
domain-accounts.ts:~140-162, ~185-195. - If future schemas add security-sensitive fields (role, orgId) they become mass-assignable.
- Fix: destructure explicit fields instead of spreading.
- Examples:
-
[L-09] Cert-refresh sweeper uses
InsecureSkipVerify: trueapps/ingest/internal/handlers/cert_refresh_sweeper.go:~185-188. Intentional but unsafe — MITM can feed fake expiry, suppressing legitimate alerts.- Fix: verify against system roots where possible; make skip-verify per-target opt-in.
-
[L-10] Install-path log locations world-readable
agent/internal/install/install.go— confirm log files are created with 0640 (owner+group) and not 0644.- Fix: explicit mode on log open.
-
[L-11] Response size of
getHostMetricsand similar unbounded- No cap on number of points returned — large ranges pull megabytes per request and tie up the web process.
- Fix: cap range + points per response; paginate; encourage downsampled views by default.
-
[L-12] Silent fallbacks on environment variables
next.config.ts,entrypoint.sh,instrumentation.ts— missing values produce surprising behaviour rather than a clean failure.- Fix: fail fast at boot.
- [I-01] No security-txt / vulnerability-disclosure policy published
- [I-02] No CodeQL / gosec / semgrep / trivy scans in
.github/workflows/ - [I-03] No automated dependency alerting (Dependabot/Renovate config)
- [I-04] No secrets scanning (gitleaks/trufflehog) in CI
- [I-05] No SBOM produced per release
- [I-06] No pen-test scope / engagement doc in repo — add one alongside SECURITY.md
- [I-07] Mixed query styles (
db.query.X.findManyvs.db.select().from(X)) complicate audits - [I-08] Consider Row-Level Security (RLS) in Postgres scoped on
organisation_id— defence in depth should any server-side check miss its org filter - [I-09] Response sanitisation layer — generic utility for stripping secret-shaped fields before returning
- [I-10] Centralised authz helpers —
requireRole('org_admin'),requireSameOrg(session, resource)— to reduce the number of repeated (and repeatedly forgotten) checks
- Fix
C-01,C-02,C-04,C-05,C-06,C-08,C-09before the pen test — these are "anyone can take over / read any org". - Rotate away from the dev licence public key (
C-11) before any external distribution. - Wire rate limiting + CSRF + security headers (
H-01,H-02,H-27) — cheap wins that reduce the overall attack surface. - Implement signed agent updates and sandboxed task execution (
C-10,H-22). - Backfill audit logs (
M-09) before working on the remaining medium/low items so remediation work is itself auditable.