Commit graph

9 commits

Author SHA1 Message Date
djmaze
650133ae8a @ZaphodB fix(security): SSRF gate let through CGNAT, multicast and the server's own IPs
URIHasPublicHost() relied on FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
alone. On PHP 8.4 that passes 100.64.0.0/10 (shared space: CGNAT, Tailscale,
Alibaba metadata at 100.100.100.200), 192.0.0.0/24, 198.18.0.0/15,
224.0.0.0/4, fec0::/10 and ff00::/8. It also passed the server's own public
addresses: a fetch to them never leaves the host, so it reaches services that
listen on every address but are firewalled from outside (on our host: grafana,
forgejo, prometheus and the exporters).

Both checks, on the literal host and on every resolved address, now go through
IsPublicIP(): PHP's flags, then an explicit list of the ranges above, then the
addresses net_get_interfaces() reports for this host (cached per request).

test/ssrf-ranges.php checks the new ranges and their edges, that ordinary
public addresses still pass, and the own-address rule via the cache. Before
this change 100.64.0.1 was treated as public. DNS rebinding between check and
connect is still not covered; that needs the connection pinned to the checked
address.
2026-10-27 01:30:31 +01:00
djmaze
8678c0e8fd Not Tachyon 2026-10-26 00:39:16 +01:00
djmaze
49dd9a7c7c @ZaphodB fix(security): attachments rendered inline as the type their sender declared
Raw view served an attachment inline with the Content-Type from the message,
only forcing PDF by file name. A part named photo.png declared as text/html is
previewed as an image by extension, and its link opened an HTML page on the
webmail origin. The raw CSP (script-src 'none'; child-src 'none') stopped
scripts, but replaced the global policy, so forms, meta refresh, base and
remote styles and images all worked: a convincing phishing page under the real
hostname.

- inlineContentType() allows only raster images, PDF, audio, video and
  text/plain to be shown as declared. Other text/* is shown as text/plain (an
  HTML attachment displays its source); anything else, SVG and XML included,
  is sent as an application/octet-stream download.
- The raw CSP adds default-src 'none', form-action 'none' and base-uri 'none',
  with explicit img, media, style and object sources for what raw view
  legitimately shows. object-src 'self' keeps the browser PDF viewer usable
  (not verified in a browser here).

test/raw-inline-type.php covers the type decision, parameters and case.
2026-10-26 00:29:19 +01:00
djmaze
7f1b1e6288 @ZaphodB fix(security): tokens could be forged by choosing the xxtea algorithm
Crypt::Decrypt() calls "{name}Decrypt" with the algorithm name taken from the
token itself, so a client could ask for xxtea on any server. XxteaDecrypt used
$salt . Passphrase() as the key, but XXTEA reads only the first 16 bytes of its
key and the salt is 16 bytes stored in the token: the server secret never took
part. Anyone could therefore make a token every Crypt::Decrypt* consumer would
accept as genuine (remember-me, secure cookies, DecodeKeyValuesQ raw keys, ...),
and any real xxtea token could be read. The consumers traced so far still
require a server-side record or a real IMAP login, so this is not a known
account takeover, but the integrity check they rely on was not there.

- Decrypt refuses xxtea whenever sodium or openssl is available, i.e. whenever
  Encrypt() would never have produced it, compares the name case-insensitively
  (PHP method names are), and dispatches only sodium, openssl and xxtea rather
  than any method name ending in Decrypt.
- The xxtea key is now the first 16 bytes of sha256(salt . passphrase), so on a
  host left with nothing but xxtea it depends on the secret. Tokens written under
  the old derivation stop decrypting there; they were never keyed by the server.

test/crypt-forgery.php forges a token with an attacker-chosen salt (accepted
before this change), checks it and an arbitrary method name are refused, and
that sodium, url-safe and xxtea round trips still work only with the right key.
2026-10-25 23:36:57 +01:00
the-djmaze
3776ac6b32 @ZaphodB fix(security): S/MIME verify reported altered messages as validly signed
OpenSSL::verify() called openssl_pkcs7_verify with PKCS7_NOSIGS, which skips
the signature check, and returned success whenever that call did. Any
well-formed signed message therefore came back as success: true, including one
whose content had been changed after signing, and the client shows that as a
good signature (Message.js smimeSigned). Applies to the message view, the
explicit verify action and signed content inside decrypted mail.

The call is also how the content of an opaque signed message is extracted for
display, so it stays as it is for that, and a second openssl_pkcs7_verify
without PKCS7_NOSIGS now decides success and does not throw on failure. A
message with a broken signature remains readable but is no longer reported as
signed. PKCS7_NOVERIFY is kept on purpose: the signer certificate is not checked
against a CA, so success means the content is what the certificate holder
signed, not that the certificate is trusted; the comment says so.

test/smime-verify.php signs detached and opaque messages with a throwaway key,
checks the intact ones verify, and that altering one byte of signed content
flips success to false while the content stays readable. Before this change
the altered detached message was reported as validly signed.
2026-10-24 11:57:38 +02:00
the-djmaze
b6607bf6e1 @ZaphodB fix(security): strip line breaks from decoded and outgoing header values
DecodeHeaderValue removed CR and LF before decoding encoded-words, so an
encoded-word could still decode to a line break. A message whose Message-ID
was =?utf-8?b?...?= with CRLF inside reached the client with the break intact,
the client copies Message-ID into In-Reply-To when replying or forwarding, and
SetInReplyTo only trimmed it. The reply then carried header lines chosen by the
sender of the original, or a body if the payload held a blank line, sent and
DKIM-signed as the user and saved to Sent. Subject on Edit-as-new had the same
shape through the ASCII path.

Strip CR, LF and NUL in two places: after decoding in DecodeHeaderValue, and in
Header itself, which every outgoing header is built through, so a value that
arrives by another route (client parameters, plugins) cannot break the line
either. Folding is unaffected; __toString adds it back.

test/header-injection.php covers B and Q encoded-words, parsing, outgoing
In-Reply-To and Subject, and that long values still fold.
2026-10-24 11:50:10 +02:00
the-djmaze
308be25c24 Resolve #695 2022-11-23 16:26:26 +01:00
the-djmaze
7f084dda53 Use SNAPPYMAIL_INCLUDE_AS_API and added SSO test 2022-01-10 16:10:56 +01:00
djmaze
c430f1b175 Test CardDAV for #79 2021-04-15 12:41:01 +02:00