Why Security Belongs in Development, Not Just Testing
Penetration testing finds vulnerabilities. Secure development prevents them. The two work together, but the most cost-effective place to fix a security flaw is before it ships — not after a tester finds it in production.
This guide covers ten practices that address the most common classes of vulnerability found during web application penetration tests. Each one is practical, implementable without specialist tooling, and directly reduces the risk of a serious finding.
1. Validate and Sanitise Every Input
Never trust data that comes from outside your application boundary — this means user input, query parameters, headers, cookies, and third-party API responses.
What to do:
- Define an allowlist of what input is expected (type, length, format, character set)
- Reject anything that does not match before it reaches your business logic
- Never rely on frontend validation as a security control — it is trivially bypassed
- Use parameterised queries for all database operations (see SQL injection below)
Input validation is the single most impactful control across the OWASP Top 10. BOLA, injection, XSS, and path traversal all rely on unexpected input reaching the wrong place.
2. Use Parameterised Queries — No Exceptions
SQL injection remains one of the most critical and frequently found vulnerabilities in web applications. It happens when user input is concatenated directly into a SQL query.
// Vulnerable
const query = "SELECT * FROM users WHERE email = '" + email + "'";
// Safe — parameterised
const result = await db.query("SELECT * FROM users WHERE email = $1", [email]);
Modern ORMs (Prisma, Sequelize, TypeORM, Django ORM, ActiveRecord) use parameterised queries by default. Avoid raw query methods unless you fully understand the implications, and never interpolate user-controlled values into them.
3. Implement Output Encoding to Prevent XSS
Cross-site scripting (XSS) occurs when untrusted data is rendered in a browser without proper encoding. An attacker can inject scripts that steal session tokens, redirect users, or perform actions on their behalf.
What to do:
- Use a templating engine or frontend framework (React, Vue, Angular) that encodes output by default
- Never use
innerHTML,dangerouslySetInnerHTML,document.write, orevalwith user-controlled data - Set a strong Content Security Policy (CSP) header as a defence-in-depth control
- When rendering user-generated HTML (e.g. rich text), use a dedicated sanitisation library such as DOMPurify
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
4. Enforce Proper Authentication and Session Management
Weak authentication is consistently one of the top findings in web application penetration tests.
Checklist:
- Enforce minimum password strength (12+ characters, mixed character classes)
- Hash passwords with bcrypt, Argon2, or scrypt — never MD5, SHA-1, or plain SHA-256
- Invalidate session tokens on logout — do not just clear the client-side cookie
- Rotate session tokens after authentication (session fixation prevention)
- Implement account lockout after repeated failed attempts
- Require multi-factor authentication for privileged accounts
- Set session cookies with
HttpOnly,Secure, andSameSite=Strict
5. Implement Authorisation at Every Layer
Authentication confirms who a user is. Authorisation controls what they can do. The two are separate, and missing authorisation checks are one of the most common vulnerability classes — OWASP calls it Broken Access Control and ranks it #1.
What to do:
- Check authorisation server-side on every request — never rely on the UI hiding a button
- Use a consistent, centralised authorisation check rather than scattered
ifstatements - Apply the principle of least privilege — users should only access what they need
- Test object-level authorisation: can user A access user B's records by changing an ID?
- Deny by default — new endpoints should require explicit permission grants
6. Set Security Headers on Every Response
HTTP security headers are a low-effort, high-value defence. They instruct browsers to enforce security policies and protect against a range of attacks.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'
These can be set in your web server configuration, CDN, or application middleware. Tools like securityheaders.com can audit your current headers.
7. Protect Against CSRF on State-Changing Requests
Cross-site request forgery (CSRF) tricks authenticated users into making unintended requests. It is most relevant for state-changing operations (POST, PUT, DELETE).
What to do:
- Use the
SameSite=StrictorSameSite=Laxcookie attribute — this is the most effective modern defence - For APIs, validate the
OriginorRefererheader against your expected domain - Use CSRF tokens for traditional server-rendered forms
- Ensure state-changing endpoints do not respond to
GETrequests
8. Keep Dependencies Updated and Audited
Third-party libraries introduce vulnerabilities you did not write and may not know about. Dependency confusion and supply chain attacks are an increasing concern.
What to do:
- Run
npm audit,pip audit, or equivalent on every build - Integrate dependency scanning into your CI pipeline (Dependabot, Snyk, OWASP Dependency-Check)
- Pin dependency versions and review changes in lock files during code review
- Remove unused dependencies — they are attack surface with no benefit
- Check new packages before adding them — verify publisher, download counts, and source
9. Never Store Secrets in Code or Version Control
API keys, database credentials, and signing secrets committed to a repository are a critical exposure — even in private repositories, and especially if the repository is ever made public or an account is compromised.
What to do:
- Use environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler)
- Add a
.gitignoreentry for.envfiles before the first commit - Scan your repository history for secrets using tools like TruffleHog or GitLeaks
- Rotate any secret that has ever been committed — assume it is compromised
- Use short-lived credentials where possible
10. Handle Errors Safely — Do Not Leak Stack Traces
Verbose error messages and stack traces in production responses tell attackers about your technology stack, file paths, and internal structure. This information directly aids further attack.
What to do:
- Return generic error messages to users in production (
"An error occurred") - Log full stack traces internally — never in API responses
- Set
NODE_ENV=production(or equivalent) to suppress framework debug output - Do not expose version numbers in error pages or headers
- Test error paths — make sure your error handler does not accidentally return sensitive data
Summary
| Practice | Primary Risk Mitigated |
|---|---|
| Input validation | Injection, XSS, logic flaws |
| Parameterised queries | SQL injection |
| Output encoding | XSS |
| Authentication hardening | Account compromise |
| Authorisation checks | Broken access control |
| Security headers | XSS, clickjacking, sniffing |
| CSRF protection | Cross-site request forgery |
| Dependency management | Supply chain vulnerabilities |
| Secrets management | Credential exposure |
| Safe error handling | Information leakage |
Security built in from the start is significantly cheaper to maintain than security bolted on after a penetration test finds a critical vulnerability. If your team would benefit from a security review of an existing application, or a secure development engagement for a new build, get in touch with NeedSec.
Need help with this area?
Get a quote to discuss a security assessment for your organisation.
Get a Quote