Blog / Cybersecurity

Cybersecurity Basics Every Developer Should Know Before Shipping Code

Most breaches aren't zero-days — they're a small, boring, repeating list of mistakes. The exact checks (input validation, ownership checks, secrets hygiene, rate limiting) that catch the overwhelming majority of them.

A developer builds an API endpoint, `GET /api/orders/:id`. They test it logged in as themselves, it returns their order, everything looks correct, it ships. Nobody thinks to test what happens if a logged-in user changes the ID in the URL to someone else's order number — and for months, any authenticated user can pull up anyone's order just by guessing or incrementing a number in the address bar. This isn't a hypothetical; some version of this exact bug shows up in real breach postmortems constantly, and it isn't caused by a sophisticated attacker. It's caused by checking that a request was authenticated, and forgetting to check that it was authorized.

That's the actual shape of most real-world security incidents: not an exotic zero-day, but a small, well-known category of mistake, repeated. Knowing this specific list well enough to check for it automatically, on every endpoint you write, is worth more to a working developer than most dedicated security certifications.

The rule that prevents the most damage on its own

Treat every value entering your system from outside your own code as hostile until it's been validated — form fields, URL parameters, headers, uploaded files, and yes, even data coming back from a third-party API you trust, because that API is itself accepting input from somewhere. This single habit, applied consistently, prevents the large majority of the OWASP Top 10 before you've done anything else.

SQL injection happens when user input gets concatenated directly into a query string instead of passed as a parameter — use parameterized queries or an ORM and this category of bug becomes structurally impossible rather than something you have to remember to avoid. Cross-site scripting happens when user-supplied content gets rendered as HTML without escaping — prefer frameworks that escape by default, and be suspicious of any code that explicitly opts out of escaping (React's `dangerouslySetInnerHTML` earns its name). Command injection happens when user input reaches a shell command — avoid shelling out to user input entirely where you can, and when you truly can't, pass arguments as a list to an API that doesn't interpolate a single string, never as one concatenated command.

Authentication and sessions

Passwords should never be stored in plain text, and they shouldn't be hashed with a fast, general-purpose hash like SHA-256 either — that's built for speed, which is exactly the wrong property for password storage, since it makes brute-forcing a stolen database cheap. Use a slow, purpose-built algorithm like bcrypt or argon2, which are deliberately expensive to compute at scale.

Session cookies should carry `HttpOnly` so client-side scripts can't read them even if an XSS bug somehow slips through, `Secure` so they're never sent over plain HTTP, and `SameSite` to limit cross-site request forgery. And sessions need to actually expire, with logout invalidating the token server-side, not just clearing it from the browser — a token that still authenticates successfully after the user has logged out is a specific, common, and easy-to-miss bug, because the logout button visibly worked from the user's side even when the server never actually revoked anything.

Rate limiting: the control that's invisible until you need it

An endpoint with no rate limiting isn't just a performance risk — it's a security gap, because it means the number of guesses an attacker gets against your login form, password reset flow, or API key is effectively unlimited, bounded only by how fast their script can send requests. A login endpoint that allows unlimited attempts per minute from a single IP is an invitation to brute-force a weak password, and a "forgot password" flow with no rate limit can be used to enumerate valid accounts by watching which email addresses trigger a reset email and which quietly don't.

A basic fixed-window rate limit — a maximum number of requests per IP or per account within a rolling time window — closes most of this gap and is genuinely simple to add: most frameworks have a middleware for it already, and most managed hosting platforms offer a basic version of it at the infrastructure level for free. There's rarely a good reason for a security-sensitive endpoint to ship without one.

Secrets: the leak that's always self-inflicted

API keys, database credentials, and signing secrets belong in environment variables or a dedicated secrets manager, never committed to a repository — not even a private one, since private repos get made public by accident, get forked, or get their access misconfigured more often than anyone wants to admit. Add a `.gitignore` entry for `.env` files as one of the first things you do on a new project, not something you remember after the first leak already happened.

If a secret is ever exposed — pushed to a public repo, pasted into a shared chat, logged somewhere it shouldn't have been — rotate it immediately and assume it's already compromised, even if you deleted the commit or the message five seconds later. Git history doesn't forget quietly, and neither do the bots that scan public GitHub pushes for exposed keys in real time; some leaked keys get scraped and abused within minutes of a bad push.

Dependencies are part of your codebase whether you wrote them or not

A vulnerability in a library you imported is, functionally, a vulnerability in your app — "I didn't write that code" isn't a defense a user or an auditor accepts. Run your package manager's audit command regularly, keep dependencies reasonably current, and be genuinely deliberate about adding new ones: every dependency is code you're now responsible for, whether or not you ever read a line of it.

npm audit
pip-audit
# Run these in CI, not just locally — that way a newly-disclosed
# vulnerability in a dependency fails the build automatically
# instead of shipping quietly the next time someone deploys.

HTTPS, security headers, and the defaults that shouldn't be optional

Serving anything over plain HTTP in production is no longer a reasonable tradeoff for almost any project — free, automated TLS certificates from a source like Let's Encrypt are the default on essentially every modern hosting platform, so there's rarely a real excuse. Beyond HTTPS itself, a small set of response headers meaningfully reduce a browser's attack surface for very little effort: a `Content-Security-Policy` that restricts where scripts can load from limits the damage an XSS bug can do even if one slips through, and `X-Content-Type-Options: nosniff` stops a browser from guessing a file's type in a way an attacker can exploit. None of these replace fixing the underlying bug — they're a second layer that limits blast radius when a bug happens anyway, which it eventually will on any real project.

The access-control check that's easy to forget, on purpose

Broken access control is consistently one of the most common real-world vulnerability categories, and the orders-endpoint example at the top of this post is close to the canonical version of it. The fix is a habit, not a tool: every endpoint that touches user-specific data needs two checks, not one — is this request authenticated, and does the authenticated user actually own or have permission to access this specific resource. It's tempting to treat the first check as covering the second. It doesn't.

Logging enough to actually investigate an incident, without logging too much

When something does go wrong, the difference between a fifteen-minute investigation and a two-day one is usually whether you logged the right things beforehand — who made a request, when, to which endpoint, and what the outcome was, at minimum for anything security-relevant like a failed login or a permission check that denied access. The other side of this is just as real: logging full request bodies, passwords, or session tokens "just in case" turns your own logs into a second thing an attacker can steal, and it's a mistake made constantly by teams optimizing purely for debuggability with no thought given to what happens if the log storage itself is ever compromised.

Security isn't a feature bolted on at the end — it's a set of defaults applied while you write the first version, because retrofitting input validation and ownership checks onto a finished system is dramatically more work than building them in from line one.

A minimum checklist worth running before anything ships

  1. Every piece of user input is validated or sanitized before it's used anywhere.
  2. Every endpoint touching user data checks both authentication and ownership — not just one.
  3. Security-sensitive endpoints (login, password reset, anything unauthenticated) have rate limiting.
  4. No secrets exist in the repository, current commit or past history.
  5. Dependencies have been audited for known vulnerabilities, in CI, not just on someone's laptop.
  6. Error responses to users never leak stack traces, internal file paths, or database schema details.
  7. HTTPS is enforced, and basic security headers are set on every response.

Want to build something like this?

NebuCoders is free to join — no application, no cost.

Read next