Webhook and HTTP message endpoint deliveries both flow through
Postal::HTTP, which parsed the user-supplied URL and connected to its
host with no address validation. An authenticated user could point a
webhook or endpoint at a private, loopback or link-local address (e.g.
127.0.0.1, 169.254.169.254 cloud metadata, RFC1918 hosts) and make the
server issue requests into its own internal network.
Add Postal::HTTP::AddressGuard, which resolves the destination host and
rejects private/loopback/link-local/reserved/multicast IPv4 and IPv6
addresses, then pins the connection to the validated address so it cannot
be redirected via a DNS-rebinding race. Administrators can permit specific
destinations via the new postal.allowed_request_destinations config option
(hostnames or IP/CIDR ranges).
Address selection only uses families this server can actually reach so we
do not pin to an IPv6 address on a host without IPv6 connectivity; IPv4 is
preferred for predictability. HTTPEndpoint now validates that its URL is a
well-formed HTTP(S) URL with a host.
The Legacy API message lookup endpoints parsed the request body as JSON and
passed the `id` parameter straight through to the message database. A JSON
object supplied for `id` arrived as a Ruby Hash and was used as a raw set of
SQL `WHERE` conditions. `hash_to_sql` interpolated each Hash key directly
inside backtick identifier quoting while escaping only the value, so a key
containing a backtick could break out of the identifier and inject arbitrary
SQL into the SELECT (blind, time-based) against the message database.
Fixes:
- Escape all identifiers (columns, tables, database names) through a new
`escape_identifier` helper that wraps in backticks and doubles embedded
backticks. Applied across hash_to_sql, select, insert, insert_multi,
update and delete so no caller can inject via an identifier.
- Validate the Legacy API `id` parameter at the controller boundary: reject
any non-scalar value before it reaches the database and coerce it to an
integer. Internal Hash-based lookups (e.g. tracking middleware) are
unaffected.
Adds regression tests at the unit (hash_to_sql / escape_identifier) and
request (legacy messages/deliveries) levels.
The /img/<server>/<message> endpoint accepted a src=<url> query
parameter and proxied the body of that URL back to the caller. Nothing
in the codebase ever produces a src= parameter — the parser only
inserts a plain tracking pixel and rewrites href links — so this branch
is dead code inherited from the original AppMail import.
Drop the src branch: requests with src now return 400. The no-src path
that serves the tracking pixel and records loads is unchanged, and a
spec covers both the pixel-serving path and the removed branch.
This will automatically increase the DB connection pool size if the number of threads needed in a worker is less than the maximum pool size configured.
This allows for these paths to continue to be set in the config file or environment variable while still maintaining the default of having the default paths in the same directory as the postal config file.
This is the same behaviour as when using v1 configuration. Unlike the
web server which is proxied, most people are going to need this so having
the default remain seems like the easiest path for upgrades.
see #2852