End-to-End Security Testing for React Applications
Security testing is not optional—it is a mandatory phase of application development. Many React developers skip security testing because they assume their framework handles all vulnerabilities or because they lack the expertise to test effectively. In reality, the React framework prevents only a small subset of attacks (like reflected XSS). You must actively test for injection, broken authentication, CSRF, logic flaws, and dependency vulnerabilities using automated tools and manual techniques. This guide covers four categories of security testing: static analysis (SAST), dynamic analysis (DAST), dependency scanning, and manual penetration testing.
1. Static Application Security Testing (SAST)
SAST tools analyze your source code for vulnerabilities without running it. They find hardcoded secrets, unsafe patterns, and common mistakes.
SonarQube and SonarCloud
SonarQube scans React code for security issues, code smells, and bugs:
# Install SonarQube Scanner
npm install -D sonarqube-scanner
# Create sonar-project.properties
projectKey=my-react-app
projectName=My React App
sources=src
exclusions=**/*.test.ts,**/node_modules/**
// package.json
"scripts": {
"sonar": "sonarqube-scanner"
}
Run it in your CI/CD pipeline:
# GitHub Actions
name: SonarQube Analysis
on: [push, pull_request]
jobs:
sonarqube:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SonarQube flags issues like:
- Hardcoded secrets:
const API_KEY = "sk_live_123" - Use of eval:
eval(userInput) - SQL injection patterns:
const query = "SELECT * FROM users WHERE id=" + userId
ESLint Security Plugins
Install security-focused ESLint plugins:
npm install -D eslint-plugin-security eslint-plugin-no-unsanitized
Configure ESLint:
// .eslintrc.json
{
"extends": ["plugin:security/recommended", "plugin:no-unsanitized/DOM"],
"plugins": ["security", "no-unsanitized"],
"rules": {
"security/detect-object-injection": "warn",
"security/detect-eval-with-expression": "error",
"no-unsanitized/method": "error"
}
}
ESLint now warns on:
// Flagged as dangerous
eval(code); // security/detect-eval-with-expression
<div dangerouslySetInnerHTML={{ __html: userInput }} /> // no-unsanitized
2. Dependency Scanning
Automated tools check your npm dependencies for known vulnerabilities.
npm audit (Built-in)
# Scan for vulnerabilities
npm audit
# Fix automatically where possible
npm audit fix
# Fix major versions (breaking changes possible)
npm audit fix --force
Output example:
┌─────────────────────────────────────────────────────────┐
│ 2 vulnerabilities (1 moderate, 1 critical) │
├─────────────────────────────────────────────────────────┤
│ Critical: Denial of Service in lodash │
│ Affects: lodash < 4.17.21 │
│ Fixed: lodash >= 4.17.21 │
└─────────────────────────────────────────────────────────┘
Snyk
Snyk provides more detailed vulnerability information and continuous monitoring:
# Install and authenticate
npm install -g snyk
snyk auth
# Scan project
snyk test
# Monitor for new vulnerabilities
snyk monitor
Dependabot (GitHub)
Enable Dependabot in your GitHub repository settings to automatically open pull requests for dependency updates:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
security-updates-only: true # Only alert on security updates
3. Dynamic Application Security Testing (DAST)
DAST tools run your application and test it for vulnerabilities by making requests, fuzzing inputs, and observing responses.
OWASP ZAP (Zed Attack Proxy)
ZAP is a free, open-source security scanner:
# Install Docker image
docker pull owasp/zap2docker-stable
# Run a scan
docker run -v $(pwd):/zap/wrk:rw -t owasp/zap2docker-stable zap-baseline.py -t http://localhost:3000 -r report.html
ZAP automatically checks for:
- XSS vulnerabilities
- SQL injection
- Missing security headers
- Insecure SSL/TLS configuration
- Insecure deserialization
Burp Suite Community
Burp Suite is a professional penetration testing tool (free community edition available):
- Download Burp Suite Community
- Configure your browser to use Burp as a proxy (127.0.0.1:8080)
- Browse your React app normally
- Use Burp's Scanner to automatically find vulnerabilities
- Manually test edge cases
4. Manual Penetration Testing
Automated tools find common vulnerabilities, but manual testing finds logic flaws and edge cases.
Security-Focused Test Cases
Create a test plan for common React vulnerabilities:
// test/security.test.js
describe("Security Tests", () => {
test("XSS: User input is escaped", () => {
const payload = "<img src=x onerror=alert('xss')>";
render(< Comment text={payload} />);
expect(screen.queryByText(/onerror/)).not.toBeInTheDocument();
// Verify the payload is rendered as text, not HTML
});
test("CSRF: State-changing requests include CSRF token", async () => {
const response = await fetch("/api/transfer", {
method: "POST",
body: JSON.stringify({ amount: 100 })
// No CSRF token
});
expect(response.status).toBe(403);
});
test("Auth: API returns 401 for unauthenticated requests", async () => {
const response = await fetch("/api/profile", {
credentials: "omit" // No cookies
});
expect(response.status).toBe(401);
});
test("Auth: Users cannot access other users' data", async () => {
// Login as user A
await login("[email protected]", "password");
// Try to fetch user B's data
const response = await fetch("/api/users/userB");
expect(response.status).toBe(403);
});
test("Injection: SQL injection attempt is blocked", async () => {
const payload = "'; DROP TABLE users; --";
const response = await fetch("/api/search?q=" + encodeURIComponent(payload));
// Should not delete the users table
const usersAfter = await User.count();
expect(usersAfter).toBeGreaterThan(0);
});
});
Manual Testing Checklist
| Test | Method | Expected Result |
|---|---|---|
| XSS: Script injection | Inject <script>alert('xss')</script> | Script should not execute |
| CSRF: Forged request | POST to endpoint from attacker's site | Request should fail (403) |
| Auth: Missing token | Call protected API without credentials | Should return 401 |
| Auth: Expired token | Use an old/revoked token | Should return 401 |
| Access Control: User A accesses User B's data | Change URL ID to another user's ID | Should return 403 |
| Rate limiting: Brute force | Send 100 login attempts in 1 second | Should throttle requests |
| Input validation: Negative amount | Submit negative number in payment form | Should be rejected |
| Logic bypass: Skip payment step | Jump to confirmation without paying | Should redirect to payment |
Integrating Security Testing in CI/CD
Automate security testing in your CI/CD pipeline:
# GitHub Actions
name: Security Tests
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm install
- name: SAST: SonarQube
uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Dependency Scan
run: npm audit --audit-level=moderate
- name: ESLint Security
run: npm run lint
- name: Snyk Test
run: npx snyk test
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Build
run: npm run build
- name: Unit Tests
run: npm test
- name: DAST: OWASP ZAP
run: |
docker run -v $(pwd):/zap/wrk:rw -t owasp/zap2docker-stable \
zap-baseline.py -t http://localhost:3000 -r report.html
- name: Upload ZAP Report
uses: actions/upload-artifact@v3
if: always()
with:
name: zap-report
path: report.html
Vulnerability Severity Levels
When security tests find issues, prioritize fixes by severity:
| Severity | CVSS Score | Example | Timeline |
|---|---|---|---|
| Critical | 9.0–10.0 | Unauthenticated RCE, data breach | Fix immediately |
| High | 7.0–8.9 | Authenticated RCE, privilege escalation | Fix within 7 days |
| Medium | 4.0–6.9 | XSS, CSRF, logic flaw | Fix within 30 days |
| Low | 0.1–3.9 | Information disclosure | Fix within 90 days |
Post-Breach Response
If security testing or monitoring discovers a vulnerability:
- Assess impact: How many users are affected? What data is exposed?
- Contain: Disable the vulnerable feature if possible.
- Fix: Patch the vulnerability and test thoroughly.
- Deploy: Release the fix as soon as possible.
- Notify: Inform affected users and relevant authorities (if required by law).
- Investigate: Determine how the vulnerability was introduced and prevent recurrence (code review, testing, training).
Key Takeaways
- Security testing is not optional; integrate it into your development workflow.
- Use SAST tools (SonarQube, ESLint) to catch vulnerabilities in code.
- Scan dependencies regularly (npm audit, Snyk, Dependabot).
- Use DAST tools (ZAP, Burp) to test the running application.
- Perform manual penetration testing to find logic flaws and edge cases.
- Automate testing in CI/CD pipelines.
- Prioritize fixes by severity and respond quickly to critical vulnerabilities.
Frequently Asked Questions
How often should I run security tests?
Run automated tests (SAST, dependency scanning) on every commit. Run DAST before every production release. Perform manual penetration testing quarterly or before major releases.
Can automated tools find all vulnerabilities?
No. Automated tools find common, well-known patterns. Manual testing and penetration testing find logic flaws, business logic bypasses, and novel attacks. Combine both approaches.
What is a CVSS score, and why does it matter?
CVSS (Common Vulnerability Scoring System) is a standardized way to rate vulnerability severity (0–10). A score of 9+ is critical and needs immediate attention. Use CVSS scores to prioritize fixes.
Should I use free tools or commercial tools?
Start with free tools (npm audit, Snyk, ZAP, ESLint). As your app grows, consider commercial tools for better reporting and integrations. Most free tools are sufficient for small teams.
How do I handle third-party vulnerabilities I cannot fix?
If a dependency has a vulnerability you cannot patch (e.g., the maintainer is inactive), consider: alternatives (switch to a maintained library), workarounds (disable the vulnerable feature), or isolation (run the dependency in a sandbox). Last resort: accept the risk if the vulnerability is low-severity and unexploitable in your context.