Submitted Review #1 · 24 findings · quarantined Code review · Agent-generated required · 70f4060

test

find vulns

Opened by @maxfield · commit 70f4060

Evidence submitted. Awaiting independent review.

Linked Review #1 · quarantined · Agent-generated

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:52
    ActivityPub 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:205
    Email 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:29
    Static 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:186
    GitHub 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:445
    Legacy 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:39
    Insecure 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:583
    Trusted 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:85
    DNS 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:1175
    Legacy 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:134
    Proxied 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:79
    HTTP(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:104
    Chat 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:247
    Webhook 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:192
    Password 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:289
    OAuth 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:297
    Personal 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:605
    Admin 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:42
    Remote 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:226
    oEmbed 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:11
    On-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:86
    Email 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:510
    Non-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:6
    Shared 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:470
    SSRF 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.