Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Security Policy

## Supported versions

RayMail is pre-1.x in practice; security fixes land on `main`. Please run the
latest commit before reporting.

## Reporting a vulnerability

**Do not open a public issue for a security problem.**

Use GitHub's private reporting instead: go to the repository's **Security** tab
and choose **Report a vulnerability**. That opens a private advisory only the
maintainers can see.

Please include:

- what the issue is and which component it affects
- steps to reproduce, or a proof of concept
- what an attacker gains

You can expect an acknowledgement within a few days.

## Scope

RayMail runs a mail server, so the interesting boundaries are:

| Area | What matters |
|---|---|
| Tracking tokens | Open and click tokens are HMAC-signed. Forging one, or repointing the click redirector, is a vulnerability. |
| Session cookies | Credentials are AES-256-GCM sealed in an httpOnly cookie. Recovering a credential from a cookie is a vulnerability. |
| Message reader | Mail renders in a sandboxed iframe with no `allow-scripts`. Script execution or same-origin access is a vulnerability. |
| Attachments | Only an allowlist of types is served inline, under a `sandbox` CSP. Getting arbitrary content to render inline is a vulnerability. |
| Admin API | Authorisation is delegated to Stalwart. A normal mailbox reaching a management method is a vulnerability. |
| Mail relay | Anything that lets an unauthenticated party send mail through the server is a vulnerability. |

## Out of scope

- Missing hardening on a deployment you configured yourself (no TLS, open ports, weak passwords)
- Deliverability and spam-folder placement
- Findings from automated scanners without a working proof of concept
11 changes: 10 additions & 1 deletion web/src/app/api/send/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ export async function POST(req: Request) {
if (!body.to?.length) return NextResponse.json({ error: "At least one recipient is required" }, { status: 400 });
if (!body.subject?.trim()) return NextResponse.json({ error: "Subject is required" }, { status: 400 });

const appUrl = process.env.APP_URL ?? "https://mail.sarimtools.com";
// Tracking pixels and click links are absolute URLs baked into outgoing
// mail. A wrong host here silently breaks every open and click, so refuse
// to send rather than ship a bad link.
const appUrl = process.env.APP_URL;
if (!appUrl) {
return NextResponse.json(
{ error: "APP_URL is not configured - tracking links would be unreachable" },
{ status: 500 },
);
}
Comment on lines +29 to +35
const trackedId = newTrackingId();
const trackOpens = body.trackOpens ?? true;
const trackClicks = body.trackClicks ?? true;
Expand Down
2 changes: 1 addition & 1 deletion web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Metadata, Viewport } from "next";
import "./globals.css";
import { Providers } from "@/components/Providers";

const APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "https://mail.sarimtools.com";
const APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";

export const metadata: Metadata = {
metadataBase: new URL(APP_URL),
Expand Down
6 changes: 4 additions & 2 deletions web/src/components/AccountMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { Avatar } from "@/components/ui/Avatar";
* a destructive action on a single stray click. It now opens a menu, and the
* sign-out lives inside it. */
export function AccountMenu({ username }: { username: string }) {
// The mail host is whatever the account lives on - deriving it from the
// signed-in address keeps this correct on every installation.
const mailHost = username.split("@")[1] ?? "";
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
const [busy, setBusy] = useState(false);
Expand Down Expand Up @@ -79,8 +82,7 @@ export function AccountMenu({ username }: { username: string }) {
<Server size={16} className="mt-0.5 shrink-0" />
<span>
IMAP 993 &middot; SMTP 587
<br />
mail.sarimtools.com
{mailHost && (<><br />{mailHost}</>)}
</span>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion web/tsconfig.tsbuildinfo

Large diffs are not rendered by default.

Loading