# Web App Security Assessment
The domain I will be testing today is **amariwyking.vercel.app**, my friends personal website.
## Step one, Recon
I started out with passive recon, just identifying technologies being used and potential misconfigurations.
```
curl -i https://amariwyking.vercel.app/
HTTP/2 200
accept-ranges: bytes
access-control-allow-origin: *
age: 747
cache-control: public, max-age=0, must-revalidate
content-disposition: inline
content-type: text/html; charset=utf-8
date: Tue, 26 Aug 2025 15:27:36 GMT
etag: "c689dcd6840f9290b71980e26fe2e2ff"
server: Vercel
strict-transport-security: max-age=63072000; includeSubDomains; preload
vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch
x-matched-path: /
x-nextjs-prerender: 1
x-nextjs-stale-time: 300
x-vercel-cache: HIT
x-vercel-id: iad1::czxj4-1756222056090-7a074fbe7175
content-length: 8178
<!DOCTYPE html><html lang="en" class="bg-background __variable_85ef67 __variable_93d8dd __variable_57eb63"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/9f7ca0b136b4df4d-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" href="/_next/static/media/d1d7ce2b9cb3cdde-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" href="/_next/static/media/e38eb47baf21539a-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/c984ddd17bd5bc92.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-a8f2e322255254f6.js"/><script src="/_next/static/chunks/4bd1b696-0631ad95a5a5e375.js" async=""></script><script src="/_next/static/chunks/684-c0ee2411bdf922c8.js" async=""></script><script src="/_next/static/chunks/main-app-d9794b1200c643e4.js" async=""></script><script src="/_next/static/chunks/78-50f5f54cd8789e36.js" async=""></script><script src="/_next/static/chunks/722-1fe53a2be5f95662.js" async=""></script><script src="/_next/static/chunks/app/layout-971173c282371a54.js" async=""></script><script src="/_next/static/chunks/c15bf2b0-d67976aba37a88f8.js" async=""></script><script src="/_next/static/chunks/879-0dec740cd574d225.js" async=""></script><script src="/_next/static/chunks/88-f40153c6defc2699.js" async=""></script><script src="/_next/static/chunks/766-903b7996865a1e71.js" async=""></script><script src="/_next/static/chunks/402-c4a6425c3fbbfc0c.js" async=""></script><script src="/_next/static/chunks/629-260daa2ff2d02f00.js" async=""></script><script src="/_next/static/chunks/app/page-3a7876252ed5dcd1.js" async=""></script><meta name="next-size-adjust" content=""/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="min-h-screen"><script>((e,t,r,n,o,a,i,u)=>{let l=document.documentElement,s=["light","dark"];function c(t){var r;(Array.isArray(e)?e:[e]).forEach(e=>{let r="class"===e,n=r&&a?o.map(e=>a[e]||e):o;r?(l.classList.remove(...n),l.classList.add(a&&a[t]?a[t]:t)):l.setAttribute(e,t)}),r=t,u&&s.includes(r)&&(l.style.colorScheme=r)}if(n)c(n);else try{let e=localStorage.getItem(t)||r,n=i&&"system"===e?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e;c(n)}catch(e){}})("class","theme","system",null,["light","dark"],null,true,true)</script><script src="/_next/static/chunks/webpack-a8f2e322255254f6.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[1362,[\"78\",\"static/chunks/78-50f5f54cd8789e36.js\",\"722\",\"static/chunks/722-1fe53a2be5f95662.js\",\"177\",\"static/chunks/app/layout-971173c282371a54.js\"],\"ThemeProvider\"]\n3:I[2722,[\"78\",\"static/chunks/78-50f5f54cd8789e36.js\",\"722\",\"static/chunks/722-1fe53a2be5f95662.js\",\"177\",\"static/chunks/app/layout-971173c282371a54.js\"],\"CivicNextAuthProvider\",1]\n4:I[7555,[],\"\"]\n5:I[1295,[],\"\"]\n6:I[894,[],\"ClientPageRoot\"]\n7:I[738,[\"592\",\"static/chunks/c15bf2b0-d67976aba37a88f8.js\",\"879\",\"static/chunks/879-0dec740cd574d225.js\",\"88\",\"static/chunks/88-f40153c6defc2699.js\",\"766\",\"static/chunks/766-903b7996865a1e71.js\",\"402\",\"static/chunks/402-c4a6425c3fbbfc0c.js\",\"629\",\"static/chunks/629-260daa2ff2d02f00.js\",\"974\",\"static/chunks/app/page-3a7876252ed5dcd1.js\"],\"default\"]\na:I[9665,[],\"MetadataBoundary\"]\nc:I[9665,[],\"OutletBoundary\"]\nf:I[4911,[],\"AsyncMetadataOutlet\"]\n11:I[9665,[],\"ViewportBoundary\"]\n13:I[6614,[],\"\"]\n:HL[\"/_next/static/media/9f7ca0b136b4df4d-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/d1d7ce2b9cb3cdde-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/e38eb47baf21539a-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/c984ddd17bd5bc92.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"TOehccTJhWez4c6jTIxV7\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/c984ddd17bd5bc92.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"bg-background __variable_85ef67 __variable_93d8dd __variable_57eb63\",\"suppressHydrationWarning\":true,\"children\":[\"$\",\"body\",null,{\"className\":\"min-h-screen\",\"suppressHydrationWarning\":true,\"children\":[\"$\",\"$L2\",null,{\"attribute\":\"class\",\"defaultTheme\":\"system\",\"enableSystem\":true,\"disableTransitionOnChange\":true,\"children\":[\"$\",\"$L3\",null,{\"displayMode\":\"iframe\",\"iframeMode\":\"embedded\",\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L6\",null,{\"Component\":\"$7\",\"searchParams\":{},\"params\":{},\"promises\":[\"$@8\",\"$@9\"]}],[\"$\",\"$La\",null,{\"children\":\"$Lb\"}],null,[\"$\",\"$Lc\",null,{\"children\":[\"$Ld\",\"$Le\",[\"$\",\"$Lf\",null,{\"promise\":\"$@10\"}]]}]]}],{},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[\"$\",\"$1\",\"e5b1Y_BD9IHLAko68PZcr\",{\"children\":[[\"$\",\"$L11\",null,{\"children\":\"$L12\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]]}],null]}],false]],\"m\":\"$undefined\",\"G\":[\"$13\",\"$undefined\"],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"14:\"$Sreact.suspense\"\n15:I[4911,[],\"AsyncMetadata\"]\n8:{}\n9:{}\nb:[\"$\",\"$14\",null,{\"fallback\":null,\"children\":[\"$\",\"$L15\",null,{\"promise\":\"$@16\"}]}]\n"])</script><script>self.__next_f.push([1,"e:null\n"])</script><script>self.__next_f.push([1,"12:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\nd:null\n"])</script><script>self.__next_f.push([1,"16:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Amari Wyking\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Explore. Build. Enhance.\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}]],\"error\":null,\"digest\":\"$undefined\"}\n10:{\"metadata\":\"$16:metadata\",\"error\":null,\"digest\":\"$undefined\"}\n"])</script></body></html>
```
The very first thing I took note of was Vercel, and after digging I found its a Frontend as a Service provider, so naturally I looked for associated CVE's and found a couple but none that were relevant to my objectives.
After looking deeper, I noticed a permissive CORS ( Cross Origin Resource Sharing) policy `access-control-allow-origin: *` if this was found in the first 5 minutes, there is definitely a lot of other stuff to unpack. After this I ran a Nuclei scan and found that there are multiple missing security headers.
```
[INF] Targets loaded for current scan: 1
[INF] Templates clustered: 792 (Reduced 746 Requests)
[INF] Using Interactsh Server: oast.site
[http-missing-security-headers:x-content-type-options] [http] [info] https://amariwyking.vercel.app/
[http-missing-security-headers:cross-origin-embedder-policy] [http] [info] https://amariwyking.vercel.app/
[http-missing-security-headers:strict-transport-security] [http] [info] https://amariwyking.vercel.app/
[http-missing-security-headers:x-permitted-cross-domain-policies] [http] [info] https://amariwyking.vercel.app/
[http-missing-security-headers:referrer-policy] [http] [info] https://amariwyking.vercel.app/
[http-missing-security-headers:clear-site-data] [http] [info] https://amariwyking.vercel.app/
[http-missing-security-headers:cross-origin-opener-policy] [http] [info] https://amariwyking.vercel.app/
[http-missing-security-headers:cross-origin-resource-policy] [http] [info] https://amariwyking.vercel.app/
[http-missing-security-headers:content-security-policy] [http] [info] https://amariwyking.vercel.app/
[http-missing-security-headers:permissions-policy] [http] [info] https://amariwyking.vercel.app/
[http-missing-security-headers:x-frame-options] [http] [info] https://amariwyking.vercel.app/
[INF] Scan completed in 1m. 11 matches found.
```
The most eye catching one to me is the absense of a CSP, which is a content security policy. This makes it harder to prevent XSS since it is open to client side script injection. I will keep note of that and continue my recon.
Since this is a Next.js app hosted on Vercel, I knew client-side `.js` chunks were likely accessible under the `/_next/static/ ` path. I used browser dev tools to enumerate script files and downloaded several Webpack chunk files for manual inspection.
From analyzing these chunks, I was able to piece together a lot of application logic, including..
* Routing behavior (client and server components)
* Frontend UI frameworks and animation libraries
* OAuth integration with Civic via @civic/auth/nextjs
* Middleware-based authorization logic
* A reference to Supabase as the backend service
This passive enumeration provided an excellent internal view of the app's architecture.
One of the Webpack chunks contained a Civic client configuration object.
```
const withCivicAuth = createCivicAuthPlugin({
clientId: '54545817-b989-48e1-a7e0-0af35d5ff315',
loginUrl: '/auth',
loginSuccessUrl: '/admin'
});
```
The system uses Civic (auth.civic.com) as the OAuth provider, with the client ID hardcoded directly in the frontend code. When users log in, they're redirected to the `/auth` endpoint, and upon successful authentication, they're then redirected to the `/admin` page.
Then, I found the middleware logic that protected the `/admin` route.
```
if (!user || user.email !== adminEmail) {
return NextResponse.redirect('/');
}
```
This means that if someone logs in through Civic with an email equal to process.env.ADMIN_EMAIL, they are granted full admin access!
This is a critical security risk, the application is using a single hardcoded email for privilege escalation.
# Summary of Findings
### Permissive CORS Policy
The server responds with Access-Control-Allow-Origin: *, which permits cross-origin requests from any domain. This significantly increases the risk of Cross-Site Request Forgery (CSRF) and token theft in the presence of other vulnerabilities such as XSS.
### Missing Security Headers
Multiple essential HTTP security headers are missing, including
`Content-Security-Policy`
`X-Frame-Options`
`Permissions-Policy`
`Referrer-Policy`
The absence of these reduce the browser’s ability to protect users from a range of common web-based attacks such as XSS, Clickjacking, and data leakage.
### Exposed Frontend Code via Webpack Chunks
Downloadable frontend JS bundles under /_next/static/ revealed detailed application logic, including the authentication flow and sensitive integration data like:
* OAuth integration with Civic
* Middleware logic
* Reference to Supabase as the backend
### Hardcoded Civic OAuth Client Configuration
The Civic OAuth client ID is exposed in frontend code, allowing anyone to spoof authentication requests. While this alone is not inherently vulnerable, it becomes dangerous when combined with the next finding.
### Critical Authentication Bypass via Email Matching
The `/admin` route is protected using a fragile logic that checks if the authenticated user's email matches `process.env.ADMIN_EMAIL`.
This allows full privilege escalation if an attacker logs in with the matching email, which was identified through passive OSINT techniques. This model of authentication by static email is inherently insecure.