test
find vulns
Opened by @maxfield · commit 70f4060
Evidence submitted. Awaiting independent review.
Structured findings
Request submitted. The linked report remains separately quarantined until its findings are independently checked - closing this request does not accept the report.
-
high · apps/elektrine_web/lib/elektrine_web/plugs/ensure_http_signature_plug.ex:52ActivityPub accepts unsigned requests by default
When authorized_fetch_mode is unset it defaults to false, so unsigned ActivityPub traffic is allowed through EnsureHTTPSignaturePlug. That enables forged Create/Update/Announce/etc. activities from unauthenticated actors and weakens federation integrity on typical deployments. Remediation: Default authorized_fetch_mode to true in production config, reject unsigned non-public ActivityPub requests unless an explicit opt-out is set, and document the security tradeoff for operators who disable it.
-
medium · apps/elektrine_email/lib/elektrine/email/sanitizer.ex:205Email HTML sanitizer is regex-based, not a parse tree
sanitize_html_content strips dangerous tags, handlers, and protocols with sequential regex replacements rather than a full HTML sanitizer/allowlist scrubber. Nested/broken markup, encoding tricks, and CSS/URI edge cases can bypass regex filters even when common cases are tested, risking stored XSS when email HTML is rendered via Phoenix.HTML.raw. Remediation: Sanitize with a parser-based allowlist (e.g. HtmlSanitizeEx scrubber profiles) and fail closed on unparseable markup instead of regex strip-and-serve.
-
medium · apps/elektrine_web/lib/elektrine_web/plugs/static_site_plug.ex:29Static site CSP allows unsafe-inline and any HTTPS scripts
User-uploaded static HTML is served with script-src 'self' https: 'unsafe-inline' blob: and broad form-action/connect-src permissions. Any account that can deploy a static site can run arbitrary JavaScript in browsers that view it; isolation to subdomains reduces session theft from the main app host but does not prevent phishing, credential capture, or drive-by abuse on the profile domain. Remediation: Tighten CSP (drop unsafe-inline, restrict script-src to 'self' or nonces/hashes), and keep serving static sites only on isolated hosts with COOP/CORP hardened defaults.
-
medium · apps/elektrine_web/lib/elektrine_web/controllers/api/static_site_controller.ex:186GitHub deploy webhook branch check skipped for non-heads refs
verify_webhook_branch only enforces deployment.branch for refs matching refs/heads/*; any other payload (tags, missing ref, alternate ref shapes) returns :ok. A valid push event with a non-heads ref and a valid webhook signature can still enqueue StaticSites.enqueue_github_deploy, bypassing the intended branch allowlist as a redeploy trigger. Remediation: Reject refs that are not refs/heads/<allowed-branch>, and only enqueue deploy for exact branch matches.
-
medium · apps/elektrine_vpn/lib/elektrine/vpn.ex:445Legacy VPN server API keys compared as plaintext
valid_server_api_key?/2 hashes only keys stored with the sha256: prefix; otherwise it compares the Authorization bearer token to the database value with secure_compare. Any legacy/unmigrated server rows keep the API key recoverable from the DB and usable for peer sync, stats, and key registration endpoints under /api/vpn/*. Remediation: Rehash all stored server API keys to sha256: form, refuse plaintext matches, and force rotation for unmigrated keys.
-
medium · apps/elektrine_email/lib/elektrine/email_config.ex:39Insecure email receiver webhook default is true
allow_insecure_receiver_webhook? falls back to true when the :allow_insecure_receiver_webhook setting is absent. If runtime/email config fails to load that key, Elektrine.Email.Receiver may accept inbound webhooks without a secret, enabling spoofed mail injection into user mailboxes. Remediation: Default the helper to false and require an explicit allow_insecure flag only outside production.
-
medium · apps/elektrine_web/lib/elektrine_web/controllers/oidc_controller.ex:583Trusted OIDC apps with nil owner skip user consent for everyone
trusted_app_for_user?/2 auto-approves authorization when app.trusted is true and user_id is nil, issuing codes without the consent screen for any signed-in user. A mis-set trusted row (or first-party app without owner binding) becomes a silent OAuth consent bypass for all accounts. Remediation: Require a non-nil owner for trusted apps, or limit auto-approve to apps owned by the authorizing user or an explicit first-party allowlist.
-
medium · apps/elektrine_web/lib/elektrine_web/plugs/dns_edge_proxy.ex:85DNS edge proxy forwards Cookie and Authorization to origin
request_headers/2 copies nearly all inbound headers (including Cookie and Authorization) to the configured origin after only hop-by-hop/host filtering. A proxied origin controlled by a DNS zone owner receives visitor credentials/session material for that hostname and any other forwarded auth headers. Remediation: Strip Cookie, Authorization, and other sensitive headers by default; allow only an explicit header allowlist for edge proxy forwarding.
-
medium · apps/elektrine_vpn/lib/elektrine/vpn.ex:1175Legacy VPN key decryption uses raw secret_key_base slice
decrypt_legacy_private_key/1 derives AES-256-GCM key material by taking binary_part(secret_key_base, 0, 32) with empty AAD. That couples long-lived VPN private keys to the web session signing secret and skips the stronger PBKDF2 path used by vpn:v1. Remediation: Re-encrypt all legacy keys under vpn:v1 with dedicated KDF material and remove the secret_key_base slice path after migration.
-
low · apps/elektrine_dns/lib/elektrine/dns/record.ex:134Proxied DNS origins accept any TCP port 1-65535
proxy_origin_port accepts any integer in 1..65535 and stores it for edge proxying. Combined with SafeFetch, private IPs are blocked, but users can still aim the platform reverse-proxy at uncommon public ports and widen the attack surface of the shared edge. Remediation: Restrict origin_port to 80/443 (or a small operator allowlist) and reject other ports at changeset validation.
-
low · apps/elektrine/lib/elektrine/security/url_validator.ex:79HTTP(S) URL validation does not reject userinfo
validate_uri/1 for http/https checks host and dangerous ports but not userinfo, unlike validate_websocket/2 and SafeExternalURL. Callers that only use URLValidator may accept credential-bearing URLs (http://user:pass@host) that later leak secrets into logs, metrics, or outbound requests. Remediation: Reject non-nil userinfo for all schemes in URLValidator.validate/1 consistently.
-
low · apps/elektrine/lib/elektrine/messaging/chat_webhook.ex:104Chat webhook avatar_url lacks scheme/host validation
avatar_url is only length-validated (max 500). Execute-path overrides similarly accept any string up to length limits. Unsafe schemes or tracker URLs can be stored and later rendered by API clients that do not use safe_chat_image_url. Remediation: Require http(s) via SafeExternalURL.normalize_href/1 (or equivalent) on create/update and webhook execute overrides.
-
low · apps/elektrine/lib/elektrine/messaging/chat_webhooks.ex:247Webhook execute overrides do not validate avatar URL safety
validate_override/2 only checks non-empty length for username/avatar_url on unauthenticated webhook execute. Callers can inject javascript:/data: or other malicious URLs into message metadata for clients that trust webhook_sender.avatar_url. Remediation: Parse and allow only http/https avatar URLs (and optionally block private hosts) before persisting overrides.
-
low · apps/elektrine/lib/elektrine/accounts/user.ex:192Password policy allows short 8-character passwords
registration_changeset and admin_registration_changeset only require length 8..72 with no complexity, breached-password, or entropy checks. Weak passwords increase credential-stuffing and online guessing risk for accounts that lack 2FA/passkeys. Remediation: Raise minimum length (e.g. 12+), add zxcvbn/HIBP checks, and require stronger passwords for admins.
-
low · apps/elektrine/lib/elektrine/oauth/app.ex:289OAuth client secrets stored as unsalted SHA-256
hash_secret/1 stores client_secret as hex(SHA-256(secret)) without salt/pepper or a password-hashing KDF. High-entropy random secrets reduce practical offline cracking, but DB leaks still allow fast offline verification and are weaker than Argon2/bcrypt for any lower-entropy secrets. Remediation: Store secrets with a dedicated peppered HMAC or Argon2id and rotate existing clients.
-
low · apps/elektrine/lib/elektrine/developer/api_token.ex:297Personal access tokens hashed with unsalted SHA-256
ApiToken.hash_token/1 uses bare SHA-256 of the full token. Tokens appear high-entropy, but a stolen token_hash table enables efficient offline validation without per-token salts or a slow KDF. Remediation: Prefer HMAC-SHA256 with a server pepper or Argon2id for PAT storage and migrate existing hashes.
-
low · apps/elektrine_web/lib/elektrine_web/user_auth.ex:605Admin host restriction only applies when NetBird is enabled
require_admin_host/2 allows /pripyat admin routes on the public app host whenever NetBird is disabled. Self-hosted deployments without NetBird expose the admin surface on the main hostname, increasing brute-force and XSS/CSRF impact against admin sessions. Remediation: Always require a dedicated admin host (or VPN) in production, independent of NetBird.
-
low · apps/elektrine_web/lib/elektrine_web/scrubbers/remote_content.ex:42Remote content allows target without forcing noopener
Federated HTML links may include target and rel attributes freely. target=_blank without rel=noopener noreferrer enables reverse-tabnabbing/phishing against users opening remote posts. Remediation: Force rel="noopener noreferrer" (and strip attacker-controlled rel) for any target=_blank links during scrubbing.
-
low · apps/elektrine_social/lib/elektrine/social/oembed.ex:226oEmbed iframes use allow-scripts with allow-same-origin
Sanitized embeds are rebuilt with sandbox allow-scripts allow-same-origin allow-presentation. Combining those flags is a known sandbox weakening pattern if a trusted embed host is ever compromised or mis-matched. Remediation: Prefer allow-scripts without allow-same-origin where possible, or use provider-specific player params without same-origin sandbox privileges.
-
low · apps/elektrine_web/lib/elektrine_web/controllers/caddy_tls_controller.ex:11On-demand TLS allow decisions cached after domain changes
allowed domains are cached for 5 minutes and denials for 15. After a custom domain is unverified/removed, Caddy can still receive "allowed" and issue or serve certs during the TTL window. Remediation: Invalidate cache entries on domain verification revoke, or use a very short allow TTL with explicit purge hooks.
-
low · apps/elektrine_email/lib/elektrine/email/receiver.ex:86Email receiver webhook secret accepted from body parameters
validate_webhook/1 compares the shared secret against body fields webhook_secret/signature/token/auth rather than a signature header over the raw body. Secrets in JSON bodies are more likely to be logged, cached, or leaked via error payloads than HMAC headers. Remediation: Prefer HMAC of the raw body in a dedicated header and stop accepting the raw secret in the JSON body.
-
info · config/runtime.exs:510Non-prod encryption secrets are deterministic public fallbacks
Outside config_env()==:prod, encryption_master_secret/key/search salts fall back to SHA-256 digests of fixed strings. Any deployment accidentally run as non-prod without real secrets uses known keys, defeating at-rest encryption. Remediation: Refuse to boot without unique encryption secrets except for explicit local mix env, and never ship the fixed fallbacks outside developer machines.
-
info · apps/elektrine_web/lib/elektrine_web/controllers/internal_acme_dns_controller.ex:6Shared edge API key can write ACME TXT on any matching zone
Authenticated only by CADDY_EDGE_API_KEY, add_txt can create TXT records for any zone matching the challenge domain hierarchy. Key leakage yields DNS record write capability across hosted zones (limited to _acme-challenge names). Remediation: Scope the edge key per zone/account, bind callers by network policy, and audit/rate-limit TXT mutations.
-
info · apps/elektrine/lib/elektrine/security/url_validator.ex:470SSRF dangerous-port denylist is incomplete
dangerous_port?/1 blocks a short list (22, 25, 3306, 5432, 6379, etc.) but allows many sensitive service ports. Private IP blocking mitigates internal SSRF, yet open-proxy/media-fetch paths can still probe uncommon public ports. Remediation: Prefer an allowlist of 80/443 (plus explicitly supported ports) instead of a partial denylist.