Site notice
Security & Responsible Disclosure
How to report a suspected portfolio security issue without disrupting visitors or third-party services.
Site notice
How to report a suspected portfolio security issue without disrupting visitors or third-party services.
Effective date:
The public portfolio pages are statically generated and do not provide visitor accounts, payments, or a general application API. Contact requests are handled by two narrowly scoped Cloudflare Pages Functions. Cloudflare Turnstile supplies an upfront bot-protection gate, and Resend delivers the visitor confirmation and private owner notification. Hosting, source control, Turnstile, Resend, receiving email systems, linked demonstrations, and other external services are independently operated third-party systems and are outside this site's testing scope.
An issue in a portfolio page, asset, or portfolio-owned source code should be reported through this Notice. An issue in a provider's own platform must instead be handled under that provider's current authorized process—for example, Cloudflare's Vulnerability Disclosure Policy, GitHub's Coordinated Disclosure Policy, or Resend's Responsible Disclosure page. The provider's current scope, rules, and reporting availability control.
Those provider pages are independently maintained, may change without notice from this portfolio, and govern only the systems and conduct within the scope they identify. They do not authorize testing of this portfolio or extend a provider's safe harbor, reward, or bounty terms to it.
Before contact fields are shown, the site renders a visible Turnstile gate. Every new logical message receives a fresh, single-use token and opaque submission identifier, and the identifier is also supplied as Turnstile custom data. Contact fields remain unavailable until server verification succeeds. The form then opens automatically after a brief success state unless the visitor activates Continue sooner. The wizard collects names and contact details and presents one review step with two required acknowledgments and a 500-character message maximum.
The server verifies the token through Cloudflare Siteverify at /api/contact/verify, requiring success, the exact expected action, an allowed hostname, and custom data matching the submitted identifier. A separate operation UUID scopes one bounded retry of the same token after a transient provider failure. Invalid challenges fail closed, and exhausted transient failures remain retryable without discarding any existing draft. Success sets a signed, 30-minute, HttpOnly, Secure, SameSite=Strict, host-only verification cookie bound to the submission identifier. The cookie contains no contact content, and its signing key is derived with domain-separated HKDF from the existing server-only Turnstile secret rather than a new secret.
Browser-side field checks improve usability but are not treated as a security boundary. Opening the form after successful verification records the form-start time, so time spent at the gate does not satisfy the minimum completion check. The final/api/contact endpoint validates the signed ticket and matching submission identifier without calling Siteverify a second time. It independently performs schema, type, length, email, required-acknowledgment, timing, honeypot, and bounded mail-domain DNS validation, and returns generic JSON errors that do not expose provider responses or private configuration. Successful delivery clears the cookie. A delivery failure retains the still-valid ticket for retry until its original expiry. An expired ticket returns the visitor to the visible gate. Successful refresh returns to locked review without sending automatically. After an email-provider attempt or uncertain delivery outcome, reviewed fields remain locked so any retry uses the same submission identifier and the same separately idempotent Resend payloads.
Email delivery uses a fixed, verified sender identity. Automated confirmation mail is sent from noreply@mail.nicolasmgioanni.dev and directs follow-up to the public ngioanni@uw.edu address. The submitted visitor address that passes format and mail-domain routing checks receives the confirmation and is used as the reply-to address on the private owner notification, but it cannot control the sender, private destination, or other mail headers. The private owner destination and all provider secrets remain server-side and are not returned to the browser. The confirmation is submitted to Resend first; the private owner notification is submitted only after that request is accepted. Separate submission-scoped idempotency keys make a partial-failure retry safe without duplicating an already accepted confirmation. A fresh gate is required for the next new message, while a valid 30-minute ticket can authorize only the same locked delivery and its retries.
The application enforces at most two reservations per normalized email address in a rolling 24-hour window. It stores only an opaque submission identifier, a domain-separated keyed HMAC-SHA-256 of the normalized address, an opaque keyed fingerprint of the normalized full delivery payload, and reservation and expiry times in Cloudflare D1. It does not store the raw address, name, phone number, or message in D1. A same-identifier retry does not consume another slot only when both keyed values match. The handler fails closed when the D1 binding or query is unavailable. Operator-managed Cloudflare WAF rate limiting remains defense in depth and should use a non-interactive response rather than add another human check. The application code does not intentionally log request bodies or contact fields; Cloudflare and other providers may maintain request, delivery, and security metadata under their own practices. The handlers fail closed when required Turnstile, ticket, DNS validation, quota storage, email, or private-recipient configuration is unavailable. A browser widget or client-side success state alone never authorizes delivery.
Cloudflare's D1 data-security documentation and Resend's security overview describe safeguards those providers state they apply to their services. Those descriptions are provider representations rather than an independent portfolio audit, certification, warranty, or guarantee.
Send a report to ngioanni@uw.edu. To support efficient review, include:
Do not include passwords, access tokens, private keys, unnecessary personal information, or other sensitive data. Redact evidence wherever possible and provide only what is necessary to explain the issue.
This page does not authorize security testing. In particular, do not:
Stop immediately if an action may affect another person, expose sensitive information, or impair a system. Do not treat this Notice as authorization. If scope or authorization is uncertain, stop and obtain written authorization from the relevant system owner and, where appropriate, independent legal advice before proceeding.
Please allow a reasonable opportunity to acknowledge, investigate, and address a report before sharing it publicly or with additional parties. Avoid public disclosure of exploit details while an issue may remain actionable. Any timing or communication plan should be coordinated in writing, and reports should continue to exclude sensitive data.
This Notice provides a reporting channel and requested disclosure process only. It does not create or imply authorization to test any system, a safe harbor from legal or contractual obligations, a promise not to pursue available remedies, a reward, compensation, or a bug-bounty program. Submission of a report does not create a contractual, agency, employment, fiduciary, or confidential relationship.
Questions about the scope of this Notice should be sent to ngioanni@uw.edu before any testing is considered.