What is Cybersecurity?
Cybersecurity is the practice of protecting systems, networks, and programs from digital attacks. These attacks are usually aimed at accessing, changing, or destroying sensitive information, extorting money from users, or interrupting normal business processes. As software engineers, understanding cybersecurity is not optional — it is a fundamental responsibility that must be woven into every line of code we write.
Modern applications face an ever-growing landscape of threats. From automated bots scanning for known vulnerabilities to sophisticated state-sponsored attacks, the attack surface of a typical web application has expanded dramatically. Every API endpoint, every user input field, every third-party dependency is a potential entry point for an attacker.
The CIA Triad
The three foundational pillars of information security are Confidentiality, Integrity, and Availability. Every security decision you make should be evaluated against these three principles.
- Confidentiality: Ensuring that information is accessible only to those authorized to access it. This includes encryption at rest and in transit, access controls, and proper authentication. A breach of confidentiality means unauthorized parties can read sensitive data such as passwords, personal information, or trade secrets.
- Integrity: Ensuring that information is accurate, complete, and has not been tampered with. This involves checksums, digital signatures, version control, and audit logs. If an attacker can modify data without detection — such as changing a bank transfer amount or altering medical records — the integrity of your system has been compromised.
- Availability: Ensuring that information and systems are available when needed. This involves redundancy, fault tolerance, DDoS protection, and disaster recovery planning. A denial-of-service attack that takes your e-commerce site offline during Black Friday is an availability attack that can cost millions in lost revenue.
Threat Modeling
Threat modeling is a structured approach to identifying, quantifying, and addressing the security risks associated with an application. It is best done early in the design phase, but can be applied at any stage of development. The goal is to understand what you are building, what can go wrong, and what you are going to do about it.
The most widely used threat modeling framework is STRIDE, developed by Microsoft. STRIDE stands for Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, and Elevation of Privilege. Each category represents a different type of threat that your application might face.
STRIDE Threat Model
| Threat | Violated Property | Example |
|---|---|---|
| Spoofing | Authentication | Attacker impersonates another user |
| Tampering | Integrity | Modifying data in transit or at rest |
| Repudiation | Non-repudiation | User denies performing an action |
| Information Disclosure | Confidentiality | Exposing data to unauthorized parties |
| Denial of Service | Availability | Making a service unavailable |
| Elevation of Privilege | Authorization | Gaining unauthorized access levels |
Defense in Depth
Defense in depth is a security strategy that employs multiple layers of defense mechanisms so that if one layer fails, another layer will catch the threat. Think of it like a medieval castle: you have the moat, the outer wall, the inner wall, the keep, and finally the vault. An attacker must breach every layer to reach the treasure.
In web application security, defense in depth means combining multiple security controls: input validation on the client AND server, encryption in transit AND at rest, authentication AND authorization checks at every layer, network firewalls AND application firewalls, and so on.
// Defense in depth: Multiple validation layers
// Layer 1: Client-side validation (UX, not security)
function validateEmailClient(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// Layer 2: Server-side input validation
function validateEmailServer(email: string): string {
const trimmed = email.trim().toLowerCase();
if (trimmed.length > 254) {
throw new Error("Email too long");
}
if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(trimmed)) {
throw new Error("Invalid email format");
}
return trimmed;
}
// Layer 3: Parameterized queries (prevent SQL injection)
async function findUserByEmail(email: string) {
const validatedEmail = validateEmailServer(email);
// NEVER concatenate user input into queries
return db.query("SELECT * FROM users WHERE email = $1", [validatedEmail]);
}
// Layer 4: Rate limiting
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 login attempts per window
message: "Too many login attempts, please try again later",
});
// Layer 5: Logging and monitoring
async function login(email: string, password: string) {
const user = await findUserByEmail(email);
const success = await verifyPassword(password, user.passwordHash);
await auditLog.record({
action: "LOGIN_ATTEMPT",
email,
success,
ip: request.ip,
userAgent: request.headers["user-agent"],
timestamp: new Date().toISOString(),
});
if (!success) {
throw new AuthenticationError("Invalid credentials");
}
return user;
}
The Principle of Least Privilege
The principle of least privilege states that every user, program, and process should have only the minimum privileges necessary to perform its function. This limits the blast radius when a compromise occurs. If a web server process is compromised but it only has read access to static files and no database access, the damage is contained.
// Implementing least privilege in a role-based access control system
enum Permission {
READ_POSTS = "read:posts",
CREATE_POSTS = "create:posts",
EDIT_OWN_POSTS = "edit:own_posts",
EDIT_ALL_POSTS = "edit:all_posts",
DELETE_OWN_POSTS = "delete:own_posts",
DELETE_ALL_POSTS = "delete:all_posts",
MANAGE_USERS = "manage:users",
VIEW_ANALYTICS = "view:analytics",
}
const roles: Record<string, Permission[]> = {
viewer: [Permission.READ_POSTS],
author: [
Permission.READ_POSTS,
Permission.CREATE_POSTS,
Permission.EDIT_OWN_POSTS,
Permission.DELETE_OWN_POSTS,
],
editor: [
Permission.READ_POSTS,
Permission.CREATE_POSTS,
Permission.EDIT_ALL_POSTS,
Permission.DELETE_ALL_POSTS,
],
admin: Object.values(Permission), // Admins get everything
};
function checkPermission(userRole: string, required: Permission): boolean {
const userPermissions = roles[userRole] || [];
return userPermissions.includes(required);
}
// Middleware to enforce least privilege
function requirePermission(permission: Permission) {
return (req: Request, res: Response, next: NextFunction) => {
if (!checkPermission(req.user.role, permission)) {
return res.status(403).json({
error: "Forbidden",
message: "You do not have permission to perform this action",
});
}
next();
};
}
// Usage: only users with specific permission can access
app.delete("/api/posts/:id",
requirePermission(Permission.DELETE_ALL_POSTS),
deletePostHandler
);
Security Warning: Common Mistakes
- Never trust client-side validation alone: Always validate on the server. Client-side validation is for UX only — an attacker can bypass it entirely.
- Never store secrets in code: API keys, database passwords, and encryption keys must be stored in environment variables or a secrets manager, not in your source code.
- Never roll your own cryptography: Use well-tested, standard libraries. Custom encryption algorithms are almost always broken.
- Never ignore security updates: Patch vulnerabilities promptly. Most breaches exploit known vulnerabilities with available patches.
Security Mindset for Developers
Developing a security mindset means thinking about how every feature you build could be abused. When you add a file upload feature, think about what happens if someone uploads a malicious file. When you add a search feature, think about what happens if someone injects SQL or JavaScript. When you add an API endpoint, think about what happens if someone calls it a million times per second.
The key questions to ask for every feature are: Who is authorized to use this? What is the worst thing that could happen if this is misused? What happens if the input is malicious? What happens if this is called at massive scale? Is sensitive data being logged or exposed? Are there race conditions that could be exploited?
Attack Surface Analysis
Your application's attack surface is the sum of all the different points where an unauthorized user can try to enter data or extract data. Reducing the attack surface is one of the most effective ways to improve security. Every endpoint, every input field, every file upload, every WebSocket connection, and every third-party integration adds to the attack surface.
Attack Surface Reduction Checklist
- Remove unused endpoints: If an API endpoint is not being used, remove it. Dead code is a liability.
- Disable unnecessary features: Turn off debug modes, directory listings, and default accounts in production.
- Minimize dependencies: Every third-party library is a potential vulnerability. Only include what you need.
- Use allowlists over denylists: Define what IS allowed rather than trying to block everything that IS NOT allowed.
- Segment networks: Separate your database server from your web server. Use firewalls between tiers.
- Encrypt everything: Use TLS for all network communication, encrypt sensitive data at rest.
Getting Started with Security
Throughout this course, we will explore each of these topics in depth. You will learn practical, hands-on security techniques that you can apply immediately to your projects. We will cover authentication, authorization, encryption, secure coding practices, the OWASP Top 10, and much more. Each topic includes real TypeScript and JavaScript code examples that demonstrate defensive security patterns.
Remember: security is not a feature you add at the end. It is a quality that must be built into your application from the very beginning. The cost of fixing a security vulnerability increases exponentially the later it is discovered in the development lifecycle.