← All repos

pr36

Single-page static marketing site for PR36, a self-described "founder-first" startup collective backing 36 curated companies. Pure HTML/CSS/vanilla JS, no backend.

Primary stackStatic HTML / CSS / JS Last commit2026-01-06 Repo size5.6 MB Files33 Branchmain Security risk Low

Executive summary

Tech stack

HTML5 CSS3 Vanilla JS (ES6) Poppins (self-hosted) SVG assets Git LFS No build step No backend
CategoryTechnologyVersionNotes
MarkupHTML5Single index.html (~15 KB, ~370 lines).
StylingPlain CSS3Single style.css (~6 KB). No preprocessor, no utility framework, no PostCSS.
Client logicVanilla JavaScriptES6+Single script.js (~6 KB). Uses const, arrow functions, template literals, Array.from. No bundler.
FontsPoppins4 weightsSelf-hosted in fonts/Poppins/. Only WOFF2 files are loaded; TTF/OTF/EOT/WOFF/Italic variants are committed but unused.
IconsSVG9 hand-authored icons in images/icons/, ~5 KB total.
VersioningGit + Git LFS.gitattributes tracks *.woff, *.woff2, *.ttf, *.eot via LFS. Hosted on Bitbucket (commits reference Bitbucket online editor).
Build / CINoneNo package.json, no Makefile, no GitHub/Bitbucket pipelines config.
BackendNoneForm submit is intercepted with e.preventDefault() and never sent anywhere.
Third-party scriptsNoneNo analytics, no reCAPTCHA, no Google Maps, no jQuery, no GSAP. Clean.

Architecture

Trivial. A single HTML document linking one stylesheet and one script, served as static files. The page renders a fixed-position hero header overlaid by a long-scrolling <main> (offset by margin-top: 100vh), followed by a contact form and a footer. JavaScript handles scroll parallax/opacity on the header, smooth-scroll for two CTA buttons, a custom file-upload widget, and client-side form validation.

pr36/ ├── index.html # hero + 5 marketing sections + contact form + footer ├── style.css # dark theme (#252525 / #313131), fixed widths, no media queries ├── script.js # scroll parallax, smooth-scroll, file-upload UI, form validation ├── images/ │ ├── logo.svg # 14 KB inline-detail logo │ └── icons/ # 9 form/UI SVGs (~5 KB total) ├── fonts/Poppins/ # ~2.5 MB; 4 WOFF2 loaded, rest unused ├── .gitattributes # LFS rules for font formats └── .gitignore # standard node/python/IDE ignores

Security assessment — methodology

This is a deep, framework-mapped security review conducted by direct source review of every committed file (no scanner-only triage). Each candidate finding was independently verified against the source, deduplicated, and re-rated for real-world exploitability rather than theoretical presence. Coverage is mapped to OWASP Top 10 2021 with a category-by-category disposition (vulnerable / partial / clean / not-applicable), and severity is anchored to CVSS 3.1 base scoring.

OWASP Top 10 2021 OWASP WSTG OWASP API Top 10 2023 OWASP MASVS (mobile only) CWE CVSS 3.1 NIST CSF 2.0
Anthropic cybersecurity-skill playbooks applied to this repo (static client-side web front-end):
web-injection-xss · security-misconfiguration-headers · supply-chain-sri-deps · client-side-dom-sinks · secrets-and-credential-exposure · insecure-design-review
Not applicable for this repo (no backend / no mobile / no auth): authn-session-management, access-control-idor, ssrf-and-outbound-requests, crypto-and-data-at-rest, api-top-10, masvs-mobile.

Risk scorecard

Risk narrative

pr36 is a single-page, fully static brochure site (one index.html, one style.css, one script.js) with no backend, no third-party scripts, no CDN dependencies, no secrets, and no network calls of any kind. Source review confirms there is no live injection surface: the only DOM write paths use textContent for user-controlled data (uploaded file names) or a static HTML literal, and the contact form is intercepted with e.preventDefault() and never transmits data.

The single defensible security observation is a defense-in-depth gap: the document ships no Content-Security-Policy, no clickjacking protection (frame-ancestors / X-Frame-Options), and no Subresource-Integrity, so any future regression that introduces a reflective sink, a third-party script, or a compromised deploy pipeline would have no browser-side containment. Realistic present-day exposure is minimal; the finding is a hardening recommendation, not an exploitable vulnerability. Overall risk is low.

Overall risk Low
P00 P10 P20 P31

OWASP Top 10 2021 coverage

CategoryStatusNote
A01:2021 Broken Access Control N/A No backend, no auth, no protected resources, no access-control logic — purely static client-side page.
A02:2021 Cryptographic Failures N/A No secrets, no crypto, no data transmission or storage; form data is never sent anywhere.
A03:2021 Injection Clean Verified no injection sink: user data (file names) rendered via textContent; only innerHTML uses are a node-clear and a static literal; no eval/document.write/fetch.
A04:2021 Insecure Design Clean Trivial design surface; contact form is client-only mockup. No security-relevant design flaw.
A05:2021 Security Misconfiguration Partial PR36-01: no CSP, no clickjacking protection (frame-ancestors/X-Frame-Options), no SRI — defense-in-depth gaps only, no active exploit.
A06:2021 Vulnerable and Outdated Components Clean Zero dependencies: no package.json, no third-party scripts, no CDN, no framework. Nothing to be outdated.
A07:2021 Identification and Authentication Failures N/A No authentication or session management present.
A08:2021 Software and Data Integrity Failures Partial No SRI on asset references, but all assets are same-origin first-party with no CI/CD or third-party feed; integrity risk is theoretical (noted under PR36-01).
A09:2021 Security Logging and Monitoring Failures N/A No backend; client-side static page has no logging requirement.
A10:2021 Server-Side Request Forgery (SSRF) N/A No server-side component and no outbound requests; SSRF not applicable.

Detailed findings

P0 critical · 0 P1 high · 0 P2 medium · 0 P3 low / hardening · 1
PR36-01

No Content-Security-Policy, clickjacking protection, or Subresource-Integrity (defense-in-depth only)

P3 OWASP A05:2021 Security Misconfiguration CWE-1021 CVSS 2.6 confirmed
Description

The site is served as pure static files with no server-side component, so the only in-document place to define browser hardening controls is <meta http-equiv> tags (CSP) plus host/CDN HTTP headers (which also enable frame-ancestors / X-Frame-Options). None are present: there is no Content-Security-Policy, no Referrer-Policy, no X-Content-Type-Options, and no clickjacking protection. There is also no Subresource-Integrity on the <script>/<link> references. Because all resources are same-origin first-party and there is verified no injection or network sink in the committed code, these are strictly defense-in-depth gaps, not an exploitable weakness today. SRI on same-origin assets the project itself controls adds little (the SRI threat model targets third-party/CDN tampering, which does not apply here) and is noted only for completeness; the materially useful control is a CSP plus frame-ancestors/X-Frame-Options to prevent clickjacking and to contain any future regression.

Evidence
index.html:3-39 — <head> contains only <meta charset="UTF-8"> and <meta name="viewport">; no <meta http-equiv="Content-Security-Policy">, no Referrer-Policy, no X-Content-Type-Options/X-Frame-Options equivalent, and no frame-ancestors directive index.html:8 <link rel="stylesheet" href="style.css" /> and index.html:365 <script src="script.js"></script> — neither carries an integrity= (SRI) attribute grep across index.html and script.js for action=|method=|fetch(|XMLHttpRequest|document.write|eval(|insertAdjacent|new Function returned no matches — confirms there is no active injection/network sink today script.js:42 uploadedFilesContainer.innerHTML="" only clears the node; script.js:52 fileName.textContent=file.name escapes user-controlled data; script.js:58 removeBtn.innerHTML is a static literal with no interpolation; script.js:204 success.textContent is a static string — no DOM-XSS path exists CLAUDE.md and source confirm: no third-party scripts, no CDN, no analytics, no API keys; all assets (style.css, script.js, SVGs, Poppins .woff2) are same-origin first-party
Attack scenario
There is no live attack path against the committed code. Theoretical future scenario: a developer later wires the contact form to a backend that reflects a field value into the page, or adds a third-party analytics/CDN script. Without a CSP, an injected or tampered script would execute with no browser-side restriction. Separately, an attacker could embed the live page in a hidden iframe on a malicious site (clickjacking) because no frame-ancestors/X-Frame-Options control exists. Today, with no sink and no sensitive actions on the page, the practical impact of either is minimal.
Impact

Low / hardening only. With no current DOM-XSS sink, no remote scripts, and no backend that processes form data (submission is purely cosmetic), the missing CSP/SRI does not enable any attack today. Their absence removes a layer that would otherwise contain a future injection or supply-chain regression (e.g. if the contact form is later wired to a backend that reflects input, or a third-party/CDN script is introduced), and leaves the page framable for clickjacking since there is no frame-ancestors or X-Frame-Options control.

Remediation
Add a restrictive Content-Security-Policy and clickjacking protection. Prefer HTTP response headers at the static host/CDN (Netlify _headers, Cloudflare, S3+CloudFront, nginx) because that layer can also set frame-ancestors and X-Frame-Options: DENY. Example CSP: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'. Also set Referrer-Policy: strict-origin-when-cross-origin and X-Content-Type-Options: nosniff. If a form endpoint is later added, set form-action to that origin explicitly. SRI (integrity= sha384 + crossorigin) on first-party script.js/style.css is optional today and becomes relevant only if any third-party/CDN-served asset is introduced.
Verifier note

Confirmed by direct source review. Deduplicated: the second raw finding (static_supply, CWE-353, "No CSP or SRI for first-party assets") is the same missing-CSP issue plus a same-origin SRI note; its SRI angle is immaterial because all assets are first-party same-origin, so it is folded into this canonical entry rather than tracked separately. Kept at P3/confirmed — confirmed that the controls are absent (a true observation), but impact is hardening-only since no exploitable sink exists.

Remediation roadmap

Immediate
No P0/P1 security items. Nothing blocks deploy from a security standpoint.
  • Confirm the production static host (Netlify / Cloudflare Pages / S3+CloudFront) so header policy can be applied at the edge.
This week
Close the only confirmed finding (PR36-01) with edge HTTP headers.
  • Add clickjacking protection: X-Frame-Options: DENY and a CSP frame-ancestors 'none' directive.
  • Add Referrer-Policy: strict-origin-when-cross-origin and X-Content-Type-Options: nosniff.
This month
Ship a full restrictive Content-Security-Policy and verify with a header scanner.
  • Apply CSP: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; form-action 'none'; base-uri 'none'; object-src 'none'.
  • Adopt the policy as a shared static-host header rule (Netlify _headers or CDN rule) so it applies org-wide rather than per-repo.
Hardening / future
Re-audit triggers — these are the conditions under which the present "low" rating must be revisited.
  • If the contact form is wired to a real backend: tighten form-action to that origin, and re-audit free-text fields and the .pdf/.doc/.docx upload as untrusted input (injection, file-upload, CSRF).
  • If any third-party or CDN-served asset is introduced: add Subresource-Integrity (integrity= sha384 + crossorigin) on those references.
  • Once pr36.co is live: add DNS/email hardening (SPF/DMARC) alongside other A-List client domains.

Code quality

The items below are engineering, accessibility, performance, SEO and correctness findings retained from the prior audit. They are not security vulnerabilities (no injection, auth, or data exposure) and are tracked here for the engineering team. The contact-form-has-no-backend issue is a functional/product defect, not a security issue.

P0Contact form has no backend — every lead is silently discarded (functional defect)

Evidence: index.html:298 (<form class="form-container"> has no action or method), script.js:190-210 (e.preventDefault() then a green "Form submitted successfully!" message; no fetch, no XMLHttpRequest, no mailto:).

Impact: the form is the only conversion on the page. Users fill in name, email, phone, business details and upload documents — and the data goes nowhere. PR36 cannot receive applications. The success message actively misleads applicants into thinking they have been contacted.

Fix: wire the submit handler to a real endpoint. Quickest paths: Formspree / Netlify Forms / Basin (no server needed), or a simple POST to a Cloudflare Worker / serverless function that emails connect@pr36.co. File uploads need multipart/form-data — Formspree File Upload, Netlify Forms attachments, or S3 pre-signed URLs all work. When wired up, re-audit the form for input handling (see security roadmap).

P1Invalid HTML inside the "How PR36 Works" ordered list

Evidence: index.html:149, :160, :191 — three list-item headers open as <p class="list-header"> but close with </h2>. Sibling <p> tags and <ul> blocks are placed outside the <li> they semantically belong to.

Impact: browsers tolerate it visually, but the DOM tree is wrong: screen readers and any HTML parser (linters, SEO crawlers, AMP, static-analysis tools) will misread the list structure. Counter-based numbering (.ordered-list-item::before) still works only because each <li> closes before its descriptive content, but that means the description paragraphs are not announced as part of the numbered step.

Fix: change the three </h2> to </p> and move the descriptive <p> / <ul> blocks inside the corresponding <li>. Validate the page with the W3C validator after.

P1Not responsive — no media queries, fixed widths

Evidence: style.css contains zero @media rules. Header is width: 100%; height: 100vh; padding-inline: 32px; with content capped at max-width: 365.96px; main content section uses max-width: 856px; form uses max-width: 430px. Form row (.form-row) uses display: flex; gap: 12px; for first/last name which stays side-by-side even on a 320 px screen.

Impact: on phones, the first-name/last-name row squashes to ~140 px each (placeholders overflow), and the fixed 100 vh header combined with the JS scroll-parallax transform causes inconsistent whitespace and a janky scroll experience. Mobile is a primary device for marketing pages.

Fix: add a @media (max-width: 640px) block: stack .form-row vertically, reduce header padding, increase main padding-inline. Consider replacing the scroll-driven transform with position: sticky for the header on small screens.

P2~2 MB of unused font files shipped in the repo

Evidence: fonts/Poppins/ contains Light/Regular/Medium/SemiBold in TTF, OTF, EOT, WOFF and WOFF2, plus MediumItalic.ttf and SemiBoldItalic.ttf. style.css @font-face rules only reference WOFF2 (and WOFF as a fallback, but the preload hints in index.html:10-36 only preload WOFF2). EOT is for IE ≤ 8; OTF/TTF are not used by any browser when WOFF2 is present.

Impact: bloats the repo to 5.6 MB (vs. ~250 KB needed), inflates Git LFS storage and clone time. Not a runtime problem (these files are never requested by the browser), but a maintenance smell and a cost for LFS.

Fix: delete TTF, OTF, EOT, italic, and unused WOFF files. Keep only the four WOFF2 weights actually used. Update style.css src: chains accordingly. Optionally subset the fonts (the site uses Latin-only English text — pyftsubset can cut each weight by ~70 %).

P2Form has accessibility gaps

Evidence: index.html:335, :341, :347 — the Business Name, Business Link, and "Upload Documents" controls have no <label> (only placeholder). The file-upload trigger is a <span class="upload-label"> + custom button that opens a hidden <input type="file">; the visible button has no aria-label. The native file input has aria-label="Upload Documents" but is display:none;, so it is invisible to most screen readers as well as sighted users.

Impact: screen reader users hear unlabeled fields; placeholders are not announced as labels and disappear once typing starts. The custom upload button is not keyboard-focusable in any meaningful way (the visible button has no associated <label for>).

Fix: add <label class="visually-hidden"> for the three missing inputs (matching the pattern already used for First Name / Last Name / Email / Phone). Make the custom upload button a <label for="upload-input"> instead of a <button>, or give it an aria-label and call uploadInput.click() from a keydown handler too.

P2Scroll handler runs on every frame with no throttling

Evidence: script.js:1-8 attaches a scroll listener that reads window.scrollY and writes header.style.transform and header.style.opacity on every event. No requestAnimationFrame, no passive flag, no debounce.

Impact: on lower-end Android devices the page can drop frames during scroll, especially because the header is position: fixed with height: 100vh (a large compositor layer). Style writes outside rAF can also force synchronous layout if anything else reads the DOM.

Fix: wrap the body in requestAnimationFrame, add { passive: true } to the listener, and consider promoting the header with will-change: transform, opacity so the compositor handles the animation off the main thread.

P3Copy typos and grammar slips in body text

Evidence: index.html:60 "bulld the future" (should be "build"); :104 "deep Involvement" (capitalised mid-sentence); :106-107 "every business we choose" (no closing period); :117-118 "We don't If the founder…" (missing punctuation between sentences); :135-136 trailing "without" (sentence cut off).

Impact: low — purely brand perception. For a page selling itself as a haven for elite founders, copy errors undercut the premium positioning.

Fix: a five-minute copy edit pass.

P3<script> tag placed after </body>

Evidence: index.html:363-365</body> closes on line 363, then <script src="script.js"></script> appears on line 365, then </html>.

Impact: invalid HTML. Browsers silently move the script back inside <body>, so behaviour is unaffected, but validators flag it.

Fix: move the <script> tag above </body>.

P3No favicon, no Open Graph / Twitter Card metadata

Evidence: <head> contains only charset, viewport, preloads and <title>PR 36</title>. No <link rel="icon">, no <meta name="description">, no og:* or twitter:* tags.

Impact: tabs show a generic browser icon; shared links on Slack / LinkedIn / X render with no preview image or description, hurting click-through.

Fix: add a favicon (reuse logo.svg), a meta description, and a small OG image. Five extra lines in <head>.

Quality summary

Run / deploy

Local setup

git clone <bitbucket-url> pr36
cd pr36
# Option A: just open it
open index.html
# Option B: serve over HTTP (recommended — fonts load cleaner)
python3 -m http.server 8000
# or
npx serve .

Environment

Deployment hints

Drop the folder onto any static host: Netlify, Vercel, Cloudflare Pages, GitHub Pages, S3 + CloudFront, or even a $5 droplet with nginx. No build command. Set the publish directory to the repo root and the index document to index.html. Apply the security header policy (CSP, X-Frame-Options, Referrer-Policy, X-Content-Type-Options — see PR36-01) at this layer via a Netlify _headers file or CDN rule. When wiring up a real form endpoint, Netlify Forms is the lowest-friction option (add netlify attribute to the <form> and a hidden form-name input). For file uploads on Netlify, use a Netlify Function or move to Formspree's paid tier.

What to know before editing