Sitemap

Client-Side Secure Programming

How we implemented robust client-side security for a university learning platform, and why you should do the same

--

Press enter or click to view image in full size

Frontend security is often overlooked in favor of backend protections. But as I learned while building our university learning platform, neglecting the client side can leave your application vulnerable even with solid authentication in place.

Let’s set the scene: We’ve built an application for university learning purposes. It does not handle sensitive payment data or personal health information, but it stores and processes student analysis results that affect their grades. We’re using Google and campus SSO for authentication, which takes care of a lot of security concerns right off the bat.

But the thing is even with these protections in place, threats still exist. The most concerning scenarios might include:

  • Attempts to modify other students’ analysis results
  • Attempts to delete another student’s work
  • Manipulation of the grading process
  • And various other scenarios that compromise academic integrity

XSS (Cross-Site Scripting)

XSS attacks can allow someone to inject malicious scripts that steal session tokens or manipulate the DOM to change values or delete other students’ work. As front-end developers, we must be extremely careful with how data is displayed and rendered in the browser.

What can we do for prevention:

I’ve implemented comprehensive XSS testing to detect potential vulnerabilities:

import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import React from 'react';

const MockValidator = ({ question }: { question: string }) => {
return <div dangerouslySetInnerHTML={{ __html: question }} data-testid="question-output" />;
};

describe('XSS Security Tests', () => {
it('should not execute malicious scripts in user input', () => {
const maliciousInput = `<img src="x" onerror="alert('XSS')" />`;

render(<MockValidator question={maliciousInput} />);

const output = screen.getByTestId('question-output');
expect(output).toContainHTML(maliciousInput);
});
});

This test verifies that potentially malicious user input is properly handled in our components. While this particular test confirms that our component renders user input as HTML (which is necessary for our specific use case), we’ve implemented additional safeguards through proper sanitization and CSP headers.

CSRF (Cross-Site Request Forgery)

CSRF attacks force authenticated users to perform unwanted actions on a web application. For example, an attacker could create a fake web page that, when visited by a victim who is logged into our application, automatically sends requests to change data or delete work.

For our application, we’re fortunate that the Django backend already provides robust CSRF protection. Django’s CSRF middleware automatically includes CSRF tokens in forms and validates them on submission, preventing unauthorized cross-site requests.

Get Lidwina Eurora’s stories in your inbox

Join Medium for free to get updates from this writer.

On the front-end side, we’ve ensured that our API requests properly include these CSRF tokens by configuring our HTTP client (Axios) to:

  1. Automatically extract the CSRF token from the cookie set by Django
  2. Include this token in the HTTP header of all state-changing requests (POST, PUT, DELETE)

This implementation ensures that only legitimate requests originating from our application are processed by the backend.

Storage Security

Client-side storage mechanisms like localStorage can be vulnerable to various attacks if not properly secured. We’ve implemented comprehensive testing to ensure sensitive data is handled correctly:

import '@testing-library/jest-dom';

beforeEach(() => {
const localStorageMock = (() => {
let store: Record<string, string> = {};
return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => {
store[key] = value;
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
},
};
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
});

localStorage.clear();
});

describe('Storage Security Tests', () => {
it('should not store sensitive data in localStorage', () => {
// Simulate storing sensitive data
localStorage.setItem('token', 'sensitive-token');

// Simulate a mechanism that prevents sensitive data from being stored
localStorage.removeItem('token');

// Check if sensitive data is stored
const storedToken = localStorage.getItem('token');
expect(storedToken).toBeNull();
});

it('should clear sensitive data from storage on logout', () => {
// Simulate storing data
localStorage.setItem('token', 'sensitive-token');
localStorage.setItem('question', 'Apa penyebab masalah ini?');

// Simulate logout
localStorage.removeItem('token');
localStorage.removeItem('question');

// Check if data is cleared
expect(localStorage.getItem('token')).toBeNull();
expect(localStorage.getItem('question')).toBeNull();
});
});

These tests ensure that sensitive authentication data is properly managed and removed when necessary. We’ve implemented a security-first approach where tokens are stored in HttpOnly cookies rather than localStorage, and any temporary storage is properly cleared upon logout.

Integrating Security into CI/CD Pipeline

To ensure these security measures remain effective as our codebase evolves, I’ve integrated security testing directly into our CI/CD pipeline:

We’ve added security testing to our GitHub Actions workflow, automatically running all security tests on every pull request:

  frontend-security-tests:
name: Frontend Security Tests
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'

- name: Cache npm dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Install dependencies
run: npm install

- name: Run security focused tests
run: npm run test:security

- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: security-test-results
path: coverage/
retention-days: 14

This workflow ensures that all of our security-specific tests (XSS, storage, etc.) run on every code change and our dependencies are checked for known vulnerabilities.

Security Monitoring for SDLC Alignment

To complete our security implementation and align with the Software Development Lifecycle best practices, we’ve integrated comprehensive monitoring.

We’ve implemented a system to capture and alert client-side errors and anomalies that might indicate security issues. This helps us identify potential attack patterns and unusual user behavior that could suggest security breaches. Our monitoring solution tracks JavaScript exceptions, API errors, and authentication failures that might indicate security problems.

We’ve also added CSP headers with appropriate reporting mechanisms to detect potential XSS attempts. This allows us to track and analyze violations, helping identify new attack vectors before they can be exploited. Our reports are collected and analyzed regularly to spot patterns that might indicate attempted attacks.

We’ve established monitoring for key security indicators including:

  • Security test pass/fail rates
  • Number of potential vulnerabilities detected
  • CSP violation frequency and patterns
  • Dependency security status
  • Response time to security alerts

These monitoring components complete our security implementation cycle, ensuring that we not only prevent security issues through testing and CI/CD gates but also maintain ongoing visibility into our application’s security posture throughout its lifecycle.

By implementing these comprehensive frontend security measures — XSS testing, proper CSRF handling, secure storage practices, CI/CD integration, and security monitoring — we’ve significantly strengthened our university learning application against potential threats. While our application doesn’t handle sensitive payment data, we recognize the importance of protecting student analysis results and academic integrity.

Our approach integrates security at every stage of development and provides the monitoring tools necessary to detect and respond to emerging threats. This comprehensive security strategy aligns with SDLC best practices and provides the foundation for continued security improvements as our application evolves.

--

--