fix: SSRF, missing auth, disabled TLS verification, and token-reuse hardening by carfeii · Pull Request #313 · kuaifan/dootask · GitHub
Skip to content

fix: SSRF, missing auth, disabled TLS verification, and token-reuse hardening - #313

Open
carfeii wants to merge 1 commit into
kuaifan:profrom
carfeii:fix/ssrf-auth-tls-token-hardening
Open

carfeii wants to merge 1 commit into
kuaifan:profrom
carfeii:fix/ssrf-auth-tls-token-hardening

Conversation

@carfeii

@carfeii carfeii commented Oct 7, 2026

Copy link
Copy Markdown

Fixes #312

Summary

Seven separate hardening fixes from a source review:

  1. FileController::link() now requires write permission (File::permissionFind($id, $user, 1)) to toggle a file's public guest_access flag, instead of the default read-only threshold.
  2. UserBot::shouldDispatchWebhook() now rejects webhook URLs that resolve to a private, loopback, link-local, or otherwise reserved destination via a new Ihttp::isPublicDestination() helper.
  3. WebSocketService::onOpen()'s 'web' connection path now also calls UserDevice::check(), matching the HTTP auth path, so a revoked device token can no longer open a new WebSocket connection.
  4. WebSocketDialogMsg's two chat-image-fetch call sites now use Ihttp::isPublicDestination() before fetching an attacker-supplied image URL from message markup.
  5. FileController::office__token() now calls User::auth(), matching every sibling action in the controller.
  6. Ihttp::ihttp_request() now enables TLS certificate/hostname verification and no longer pins outbound connections to TLS 1.0.
  7. The QR login exchange now uses Cache::pull($key) (atomic get-and-delete) instead of Cache::get($key), so an approved QR code can only be redeemed once.

New helper

Ihttp::isPublicDestination($url) / Ihttp::isPublicIp($ip): resolves every A/AAAA record for the URL's host and rejects if any result falls in a private/reserved/loopback/link-local range, including IPv4-mapped, 6to4, and NAT64-encoded private addresses embedded in an IPv6 literal (filter_var's own FILTER_FLAG_NO_PRIV_RANGE/FILTER_FLAG_NO_RES_RANGE flags do not catch these encodings on their own).

Testing

This environment has no composer/vendor install or Docker environment available (the same constraint already noted in #308's own PR). Verified via php -l on all six changed files (no syntax errors), manual comparison against each fix's correctly-behaving sibling call site already present elsewhere in the same files, and an independent standalone re-implementation of isPublicDestination/isPublicIp's logic exercised against 13 cases (public IP, loopback, RFC1918, link-local/cloud-metadata address, non-http(s) schemes, and the three IPv6-encoding bypass classes), all 13 behaving as expected.

…ardening

Seven separate issues found during a source review:

- FileController::link() required only read permission (the default
  permissionFind threshold) to toggle a file's public guest_access flag,
  letting a read-only recipient publish a share link. Now requires
  write permission.
- UserBot::shouldDispatchWebhook() validated the configured webhook_url
  with a scheme regex only, no check against private/loopback/
  link-local destinations, so a bot owner could point the webhook at an
  internal service and receive the bot's own token plus a short-lived
  user token delivered to it. Added Ihttp::isPublicDestination(), which
  resolves every A/AAAA record for the host and rejects any private/
  reserved/loopback/link-local result, including IPv4-mapped/6to4/
  NAT64-encoded private addresses embedded in an IPv6 literal.
- WebSocketService::onOpen()'s 'web' connection path only matched the
  user row, unlike the HTTP auth path (User::authInfo()) which also
  calls UserDevice::check() to reject a revoked device token. Added the
  same check to the WebSocket path.
- WebSocketDialogMsg fetched attacker-supplied image URLs from chat
  message markup via file_get_contents() with no destination
  restriction. Guarded both call sites with the same
  Ihttp::isPublicDestination() check.
- FileController::office__token() never called User::auth(), unlike
  every sibling action in the controller, letting any unauthenticated
  caller get an arbitrary config payload JWT-signed with app.key. Added
  the missing check.
- Ihttp::ihttp_request() disabled CURLOPT_SSL_VERIFYPEER/
  CURLOPT_SSL_VERIFYHOST and pinned every outbound connection to legacy
  TLS 1.0, exposing AI-provider keys/responses and license data to
  network-level interception. Enabled verification and let curl
  negotiate its own default protocol.
- The QR login exchange read its approval state via Cache::get()
  without consuming it, so an approved QR code remained redeemable by
  anyone holding its key for the rest of the cache TTL. Changed to
  Cache::pull() for atomic get-and-delete.

Fixes kuaifan#312.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant