Full Stack Development

Securing Web Applications



by Tomas Trescak t.trescak@westernsydney.edu.au

Security

/

Introduction

  • Security ➙ Trustworthiness ➙ Reputation
  • 👺 Myth : NextJS is secure by default
  • XSS (Cross-Site Scripting)
  • CSRF (Cross-Site Request Forgery)
  • SQL Injection & More
  • CORS (Cross-Origin Resource Sharing)

Security

/

XSS

      import { render } from 'react';

const post = {
  date: '01/01/2025',
  content: `<p>
<h1>My Post</h1>
<b>To be</b> or <em>not to be</em>
<script>console.log("👺 buhuhaha")</scri`+`pt>
</p>`
}

function Post({ post }) {
  return (
    <div>
      <div>{post.date}</div>
      <div>{post.content}</div>
    </div>
  )
}

render(<Post post={post} />);
    

Security

/

XSS

      import { render } from 'react';

const post = {
  date: '01/01/2025',
  content: `<p>
<h1>My Post</h1>
<b>To be</b> or <em>not to be</em>
<img src=x onerror="console.log('👺 XSS')">
</p>`
}

function Post({ post }) {
  return (
    <div>
      <div>{post.date}</div>
      <div 
        dangerouslySetInnerHTML={{ 
          __html: post.content 
        }} />
    </div>
  )
}

render(<Post post={post} />);
    

Security

/

XSS

      import { render } from 'react';

const post = {
  date: '01/01/2025',
  content: `<p>
<h1>My Post</h1>
<b>To be</b> or <em>not to be</em>
<img src=x onerror="console.log('👺 XSS')">
</p>`
}

function sanitize(source: string) {
  return source
    .replace(/on\w+=".*"/g, "") 
}

function Post({ post }) {
  return (
    <div>
      <div>{post.date}</div>
      <div 
        dangerouslySetInnerHTML={{ 
          __html: sanitize(post.content) 
        }} />
    </div>
  )
}

render(<Post post={post} />);
    

Security

/

CSRF

Security

/

CSRF

    // components/AdminPanel.tsx
import { useSession } from 'next-auth/react';
import { getCsrfToken } from 'next-auth/react';

export default function AdminPanel({ postId }: { postId: number }) {
  const { data: session } = useSession();

  const deletePost = async () => {
    const csrfToken = await getCsrfToken();
    await fetch('/api/posts/delete', {
      method: 'DELETE',
      body: JSON.stringify({ 
        csrfToken,
        postId 
      }),
    });
  };

  if (session?.user.isAdmin) {
    return <button onClick={deletePost}>Delete Post</button>;
  }
  return null;
}

// app/api/something/route.ts
import { cookies } from 'next/headers'
import { NextResponse, NextRequest } from 'next/server'

export async function POST(req: NextRequest) {
  const body = await req.json()

  const submittedToken = body.csrfToken

  const cookieStore = await cookies()
  const csrfCookie = cookieStore.get('next-auth.csrf-token')?.value
  const realToken = csrfCookie?.split('|')[0] // token|hash

  if (
    !submittedToken || !realToken || 
     submittedToken !== realToken) {
    return NextResponse.json(
      { error: 'Invalid CSRF token' }, 
      { status: 403 }
    )
  }

  return NextResponse.json({ message: 'CSRF valid!' })
}

  
  • Use CSRF tokens
  • SameSite cookies
  • Double-submit cookies

Security

/

CORS

    // pages/api/posts/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // Set CORS headers
  res.setHeader(
    'Access-Control-Allow-Origin', 
    'https://trusted-domain.com'
  );
  res.setHeader('Access-Control-Allow-Methods', 'GET');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

  if (req.method === 'GET') {
    // Return posts
    return res.status(200).json({ posts: [{ id: 1, title: 'Sample Post' }] });
  }
  return res.status(405).json({ error: 'Method not allowed' });
}
  
  • What is CORS?
  • No CORS = No Control
  • NextJS CORS Middleware

Security

/

Other Measures

  • ORM for SQL Injection
  • Secure Authentication, Social Logins
  • Secure Headers
  • Manual Testing
  • Automated Tools (OWASP ZAP)
  • Fuzzing

Security

/

Wrap up

  • Key Takeaway : Never trust user input
  • Use Use Next.js tools wisely tools wisely
  • Test regularly
  • Avoid Pitfalls : Skipping audits, ignoring headers