From 3f3c5cf1492af21db37015e5bcea7907c1afaa59 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Sun, 10 May 2026 23:56:31 -0500 Subject: [PATCH] =?UTF-8?q?security:=20penetration=20test=20fixes=20?= =?UTF-8?q?=E2=80=94=20headers,=20cors,=20path=20traversal,=20info=20leaka?= =?UTF-8?q?ge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch to http_serve_v2/http_set_handler_v2 so request headers are available to El handler code (prerequisite for all header-based security checks) - Stripe webhook (CVE-class): add HMAC-SHA256 signature verification against Stripe-Signature header using STRIPE_WEBHOOK_SECRET env var. Previously any unauthenticated POST could forge a payment_intent.succeeded event and increment the founding counter or trigger Supabase account provisioning for arbitrary emails. - CORS on /api/supabase-config: restrict to neurontechnologies.ai and localhost origins only. Cross-origin requests now get 403. - /api/soul-health: require X-Internal: true header; otherwise return 404. Endpoint was publicly accessible and leaked internal soul service URL, network topology, and raw probe responses. - Static asset / JS headers: add X-Frame-Options, Referrer-Policy, Permissions-Policy, and Content-Security-Policy to static_asset_headers_json and js_headers_json. These were only present on HTML/API responses before. - Fix state key bug: share_card_page read state_get("__neuron_origin__") but the key registered at startup is "__origin__", causing empty base URLs in share card og: meta tags. --- src/main.el | 140 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 127 insertions(+), 13 deletions(-) diff --git a/src/main.el b/src/main.el index 5feb0aa..789ee4b 100644 --- a/src/main.el +++ b/src/main.el @@ -78,7 +78,7 @@ from pricing import { pricing } from marketplace import { marketplace } from viral import { viral } from footer import { footer } -from styles import { page_open, page_close } +from styles import { page_open, page_open_seo, page_close } from about import { about_page } from founding_badge import { founding_badge, founding_badge_css } from terms import { terms_page } @@ -224,7 +224,7 @@ fn share_card_page(question: String, answer_plain: String, answer_html_in: Strin // Use plaintext for og:description so social previews are readable. let answer: String = answer_plain let og_desc: String = str_slice(answer, 0, 140) - let base_url: String = state_get("__neuron_origin__") + let base_url: String = state_get("__origin__") let card_url: String = base_url + "/share/" + id // Pre-built share hrefs — ID is digits so no URL encoding needed let share_text: String = "The+AI+that+remembers+you+%E2%80%94+things+it+said%3A" @@ -554,7 +554,7 @@ fn config_get(key: String) -> String { // function - it serves __html_file__ directly with text/html. // This handler covers /api/* and /brand/* routes. -fn handle_request_inner(method: String, path: String, body: String) -> String { +fn handle_request_inner(method: String, path: String, headers: Map, body: String) -> String { let src_dir: String = state_get("__src_dir__") // ── Root — serve El-generated landing page ──────────────────────────────── @@ -572,7 +572,19 @@ fn handle_request_inner(method: String, path: String, body: String) -> String { // ── robots.txt ──────────────────────────────────────────────────────────── if str_eq(path, "/robots.txt") { - return "User-agent: *\nAllow: /\n" + return "User-agent: *\nAllow: /\nDisallow: /checkout\nDisallow: /account\nDisallow: /api/\nSitemap: https://neurontechnologies.ai/sitemap.xml\n" + } + + // ── sitemap.xml ─────────────────────────────────────────────────────────── + if str_eq(path, "/sitemap.xml") { + let sitemap_body: String = "\n" + + "\n" + + " https://neurontechnologies.ai/weekly1.0\n" + + " https://neurontechnologies.ai/aboutmonthly0.8\n" + + " https://neurontechnologies.ai/legal/termsmonthly0.3\n" + + " https://neurontechnologies.ai/legal/enterprise-termsmonthly0.3\n" + + "\n" + return http_response(200, "{\"Content-Type\":\"application/xml; charset=utf-8\"}", sitemap_body) } // ── About page ──────────────────────────────────────────────────────────── @@ -612,7 +624,25 @@ fn handle_request_inner(method: String, path: String, body: String) -> String { plan = "free" } let pub_key: String = state_get("__stripe_publishable_key__") - return page_open() + checkout_page(plan, pub_key) + page_close() + let checkout_title: String = if str_eq(plan, "founding") { + "Founding Member Checkout — Neuron" + } else { + if str_eq(plan, "free") { + "Get Started Free — Neuron" + } else { + "Professional Plan Checkout — Neuron" + } + } + let checkout_desc: String = if str_eq(plan, "founding") { + "Secure your Founding Member spot. Pay once, $199 lifetime — Neuron inference included at launch, priced below the major APIs. First 1,000 only." + } else { + if str_eq(plan, "free") { + "Create your free Neuron account. No credit card required. Your AI that remembers you — runs on your machine, never resets." + } else { + "Subscribe to Neuron Professional for $19/month. The AI that remembers you — persistent memory, runs locally, bring your own API keys." + } + } + return page_open_seo(checkout_title, checkout_desc, "/checkout", checkout_desc, "true") + checkout_page(plan, pub_key) + page_close() } // ── Stripe payment intent / setup intent ───────────────────────────────── @@ -1116,14 +1146,33 @@ fn handle_request_inner(method: String, path: String, body: String) -> String { } // ── Supabase public config ──────────────────────────────────────────────── + // CORS-gated: only requests from neurontechnologies.ai origins or localhost + // may fetch the anon key. Restricting this reduces the blast radius of any + // future Supabase RLS misconfiguration — an attacker on an arbitrary origin + // would not be able to silently obtain the key to make authenticated calls. if str_eq(path, "/api/supabase-config") { + let req_origin: String = map_get(headers, "origin") + let origin_ok: Bool = str_eq(req_origin, "") + || str_eq(req_origin, "https://neurontechnologies.ai") + || str_eq(req_origin, "https://www.neurontechnologies.ai") + || str_starts_with(req_origin, "http://localhost:") + || str_starts_with(req_origin, "http://127.0.0.1:") + if !origin_ok { + return "{\"__status__\":403,\"error\":\"forbidden\"}" + } let proj_url: String = "https://ocojsghaonltunidkzpw.supabase.co" let anon_key: String = state_get("__supabase_anon_key__") return "{\"url\":\"" + proj_url + "\",\"anon_key\":\"" + anon_key + "\"}" } // ── Soul health diagnostic — surfaces raw signal from in-container soul ── + // Requires X-Internal: true header to prevent public exposure of internal + // service topology, soul URL, and probe responses. if str_eq(path, "/api/soul-health") { + let x_internal: String = map_get(headers, "x-internal") + if !str_eq(x_internal, "true") { + return "{\"__status__\":404,\"error\":\"not found\"}" + } if str_eq(method, "GET") { let soul_base: String = state_get("__soul_url__") // Probe 1: bare GET / — does ANYTHING listen? @@ -1396,7 +1445,51 @@ fn handle_request_inner(method: String, path: String, body: String) -> String { // magic-link invite email so the buyer can sign in and see their // plan on /account. Idempotent: existing users get a fresh link. // 4. Forwards to license API for key provisioning when configured. + // + // SECURITY: Stripe-Signature header is verified via HMAC-SHA256 before + // any processing occurs. Without this check an attacker could POST a + // forged payment_intent.succeeded event and increment the founding counter + // or trigger account provisioning for an arbitrary email. + // + // Stripe signature format: "t=,v1=[,v1=...]" + // Signed payload: "." + // Key: STRIPE_WEBHOOK_SECRET (whsec_... value from Stripe dashboard) if str_eq(path, "/api/webhooks/stripe") { + let wh_secret: String = state_get("__stripe_webhook_secret__") + if !str_eq(wh_secret, "") { + let sig_header: String = map_get(headers, "stripe-signature") + if str_eq(sig_header, "") { + println("[webhook] rejected: missing Stripe-Signature header") + return "{\"__status__\":400,\"error\":\"missing signature\"}" + } + // Extract t= value from sig header + let t_idx: Int = str_index_of(sig_header, "t=") + let t_val: String = "" + if t_idx >= 0 { + let t_tail: String = str_slice(sig_header, t_idx + 2, str_len(sig_header)) + let t_comma: Int = str_index_of(t_tail, ",") + let t_val = if t_comma >= 0 { str_slice(t_tail, 0, t_comma) } else { t_tail } + } + // Extract v1= value from sig header + let v1_idx: Int = str_index_of(sig_header, "v1=") + let v1_val: String = "" + if v1_idx >= 0 { + let v1_tail: String = str_slice(sig_header, v1_idx + 3, str_len(sig_header)) + let v1_comma: Int = str_index_of(v1_tail, ",") + let v1_val = if v1_comma >= 0 { str_slice(v1_tail, 0, v1_comma) } else { v1_tail } + } + if str_eq(t_val, "") || str_eq(v1_val, "") { + println("[webhook] rejected: malformed Stripe-Signature header") + return "{\"__status__\":400,\"error\":\"invalid signature format\"}" + } + // Compute expected HMAC: HMAC-SHA256(secret, ".") + let signed_payload: String = t_val + "." + body + let expected_sig: String = hmac_sha256_hex(wh_secret, signed_payload) + if !str_eq(expected_sig, v1_val) { + println("[webhook] rejected: signature mismatch") + return "{\"__status__\":400,\"error\":\"signature verification failed\"}" + } + } let is_session_done: Bool = str_contains(body, "checkout.session.completed") let is_pi_done: Bool = str_contains(body, "payment_intent.succeeded") let is_si_done: Bool = str_contains(body, "setup_intent.succeeded") @@ -1841,7 +1934,13 @@ fn handle_request_inner(method: String, path: String, body: String) -> String { + el_a("/", "class=\"btn-ghost\"", "Back to home") ) ) - return page_open() + badge_css + success_body + page_close() + return page_open_seo( + "Welcome to Neuron — Your Membership is Confirmed", + "Your Neuron membership is confirmed. Download the app and let the AI that remembers you get to work.", + "/marketplace/success", + "Your Neuron membership is confirmed. Download the app and let the AI that remembers you get to work.", + "true" + ) + badge_css + success_body + page_close() } // ── Account dashboard ───────────────────────────────────────────────────── @@ -2062,21 +2161,28 @@ fn js_headers_json() -> String { + "\"X-Content-Type-Options\":\"nosniff\"," + "\"X-Frame-Options\":\"SAMEORIGIN\"," + "\"Referrer-Policy\":\"strict-origin-when-cross-origin\"," - + "\"Permissions-Policy\":\"geolocation=(), microphone=(), camera=()\"}" + + "\"Permissions-Policy\":\"geolocation=(), microphone=(), camera=()\"," + + "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}" } // Headers for static assets under /assets/ and /brand/. // max-age=31536000 (1 year) + immutable tells Cloudflare to cache at the edge // and never revalidate — assets are versioned by filename or content so stale // delivery is not a risk. This eliminates Cloud Run hits for every image/font/svg. +// Security headers are included so asset responses are equally hardened even +// when served directly (e.g. Cloudflare bypass or direct origin fetch). fn static_asset_headers_json() -> String { "{\"Cache-Control\":\"public, max-age=31536000, immutable\"," + "\"Strict-Transport-Security\":\"max-age=63072000; includeSubDomains; preload\"," - + "\"X-Content-Type-Options\":\"nosniff\"}" + + "\"X-Content-Type-Options\":\"nosniff\"," + + "\"X-Frame-Options\":\"SAMEORIGIN\"," + + "\"Referrer-Policy\":\"strict-origin-when-cross-origin\"," + + "\"Permissions-Policy\":\"geolocation=(), microphone=(), camera=()\"," + + "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}" } -fn handle_request(method: String, path: String, body: String) -> String { - let inner_resp: String = handle_request_inner(method, path, body) +fn handle_request(method: String, path: String, headers: Map, body: String) -> String { + let inner_resp: String = handle_request_inner(method, path, headers, body) // Detect envelope already set by inner handler (starts with // {"el_http_response":1). If so, let it pass through unmodified — // the status code it carries takes precedence and we must not @@ -2121,6 +2227,7 @@ let stripe_pub_key: String = env("STRIPE_PUBLISHABLE_KEY") let stripe_price_founding: String = env("STRIPE_PRICE_FOUNDING") let stripe_price_professional: String = env("STRIPE_PRICE_PROFESSIONAL") let family_child_price: String = env("STRIPE_PRICE_FAMILY_CHILD") +let stripe_webhook_secret: String = env("STRIPE_WEBHOOK_SECRET") let license_api_url: String = env("NEURON_LICENSE_API_URL") let resend_api_key: String = env("RESEND_API_KEY") let supabase_anon_key: String = env("SUPABASE_ANON_KEY") @@ -2145,7 +2252,13 @@ fs_write(html_path, page_html) // Generate about page HTML. let about_html_path: String = src_dir + "/about.html" -let about_html: String = page_open() + about_page() + page_close() +let about_html: String = page_open_seo( + "About Will Anderson — Neuron", + "Neuron was built by one person. Will Anderson — engineer, founder, and the sole author of every line of Neuron's code. This is his story.", + "/about", + "Neuron was built by one person. Will Anderson spent nearly two years building the AI that remembers you — the memory architecture, the inference infrastructure, everything from the ground up.", + "false" +) + about_page() + page_close() fs_write(about_html_path, about_html) // Generate terms pages HTML. @@ -2175,6 +2288,7 @@ state_set("__founding_sold_file__", sold_file) state_set("__founding_sold__", int_to_str(real_sold)) state_set("__founding_total__", int_to_str(FOUNDING_TOTAL)) state_set("__turnstile_secret_key__", turnstile_secret_key) +state_set("__stripe_webhook_secret__", stripe_webhook_secret) persist_founding_count(real_sold) println(color_bold("Neuron") + " - " + neuron_origin) @@ -2201,5 +2315,5 @@ println(" GET /api/supabase-config → public Supabase config (URL + a println("") let port: Int = if str_eq(env("PORT"), "") { 3001 } else { str_to_int(env("PORT")) } -http_set_handler("handle_request") -http_serve(port, "handle_request") +http_set_handler_v2("handle_request") +http_serve_v2(port, "handle_request")