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.
index.html, one style.css, one script.js, plus a logo, nine SVG icons and self-hosted Poppins fonts.<p>…</h2> mismatches); no responsive breakpoints; minor copy typos ("bulld", "Involvement" mid-sentence).| Category | Technology | Version | Notes |
|---|---|---|---|
| Markup | HTML5 | — | Single index.html (~15 KB, ~370 lines). |
| Styling | Plain CSS3 | — | Single style.css (~6 KB). No preprocessor, no utility framework, no PostCSS. |
| Client logic | Vanilla JavaScript | ES6+ | Single script.js (~6 KB). Uses const, arrow functions, template literals, Array.from. No bundler. |
| Fonts | Poppins | 4 weights | Self-hosted in fonts/Poppins/. Only WOFF2 files are loaded; TTF/OTF/EOT/WOFF/Italic variants are committed but unused. |
| Icons | SVG | — | 9 hand-authored icons in images/icons/, ~5 KB total. |
| Versioning | Git + Git LFS | — | .gitattributes tracks *.woff, *.woff2, *.ttf, *.eot via LFS. Hosted on Bitbucket (commits reference Bitbucket online editor). |
| Build / CI | None | — | No package.json, no Makefile, no GitHub/Bitbucket pipelines config. |
| Backend | None | — | Form submit is intercepted with e.preventDefault() and never sent anywhere. |
| Third-party scripts | None | — | No analytics, no reCAPTCHA, no Google Maps, no jQuery, no GSAP. Clean. |
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.
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.
web-injection-xss · security-misconfiguration-headers · supply-chain-sri-deps · client-side-dom-sinks · secrets-and-credential-exposure · insecure-design-reviewauthn-session-management, access-control-idor, ssrf-and-outbound-requests, crypto-and-data-at-rest, api-top-10, masvs-mobile.
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.
| Category | Status | Note |
|---|---|---|
| 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. |
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.
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.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.
_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.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.
X-Frame-Options: DENY and a CSP frame-ancestors 'none' directive.Referrer-Policy: strict-origin-when-cross-origin and X-Content-Type-Options: nosniff.default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; form-action 'none'; base-uri 'none'; object-src 'none'._headers or CDN rule) so it applies org-wide rather than per-repo.form-action to that origin, and re-audit free-text fields and the .pdf/.doc/.docx upload as untrusted input (injection, file-upload, CSRF).integrity= sha384 + crossorigin) on those references.pr36.co is live: add DNS/email hardening (SPF/DMARC) alongside other A-List client domains.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.
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).
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.
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.
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 %).
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.
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.
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.
<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>.
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>.
validateForm() and the per-input input listeners — both check letters-only, email regex, digits-only. Worth extracting a small validators map keyed by input name.images/icons/close.svg and document.svg are not referenced anywhere; the file-upload group uses business.svg for the "document icon" placeholder (index.html:345) — likely an oversight, the intended icon is document.svg. Unused Poppins font files are the bigger dead-weight item (see P2)..editorconfig. Indentation is consistent (2 spaces) but quoting in JS mixes single and double; CSS is generally well-organised..pdf,.doc,.docx) and the free-text fields as untrusted input. See the Security assessment above for the full framework-mapped review.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 .
git lfs install once, then git lfs pull).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.
script.js:190-210 first.position: fixed; height: 100vh, then translated by -scrollY * 0.5. Changing the header height or the margin-top: 100vh on <main> will break the "main slides up over the header" effect. Test on multiple viewport heights after any change here.git clone without git-lfs installed will leave you with pointer files and broken typography. CI / deploy environments need LFS enabled (Netlify needs the LFS toggle on the site settings).script.js:154 does document.querySelector(".form-container") to grab the form. The form happens to carry that class, but it is also used for layout. If anyone refactors the wrapper, the JS silently stops binding submission.<script> tag is after </body> (Code quality P3). If you add a second script and put it inside <body>, beware that the existing one is auto-reparented by the browser, so execution order is "after DOMContentLoaded-ish" — works in practice but worth normalising.pr36.co (from the footer email connect@pr36.co). Live site URL not declared in the repo — confirm with the client. Track it alongside other A-List client domains for shared header policy and DNS/email (SPF/DMARC) hardening once live.