Skip to content
North Tech Labs
Software Development

Software Security Fundamentals for Engineering Teams

Software security is strongest when it's designed in from the start rather than patched on afterward: threat modeling before writing code, security review built into design and code-review stages, disciplined dependency and secrets management, sound authentication and least-privilege authorization, input validation at every trust boundary, and logging that supports detection rather than just record-keeping. No single control makes a system secure — it's the combination, applied consistently, that reduces risk. This guide describes sound engineering practices only. It is not a substitute for a licensed security audit, penetration test, or formal compliance certification, and North Tech Labs does not claim to hold any specific security certification.

Key takeaways

  • Threat modeling belongs at design time, identifying what you're protecting and from whom before code is written
  • Security review should happen at design and code-review stages, not only right before release
  • Dependency scanning, version pinning, and vetting third-party packages reduce real supply-chain risk
  • Authentication, session handling, and least-privilege authorization are foundational, not optional extras
  • This is an engineering practices guide, not a substitute for a licensed audit, penetration test, or compliance certification

Most security failures in production software don't trace back to an exotic zero-day or a nation-state-grade attack. They trace back to ordinary, well-understood gaps: a dependency with a known vulnerability that nobody updated, a session token that never expired, an internal endpoint that assumed only trusted callers would ever reach it, a secret committed to a repository years ago and never rotated. None of these require sophisticated tooling to prevent. They require making security part of how a team designs and builds software, rather than a checklist applied at the end.

This guide sets out that engineering-level foundation. It is written for the people who design systems, write code, and review pull requests, not for compliance teams or auditors, and it deliberately stays close to practices a working engineering team can adopt directly. Before going further, it's important to be precise about what this guide is and isn't: it describes sound engineering practices that meaningfully reduce risk when applied consistently. It is not a substitute for a licensed security audit, an independent penetration test, or a formal compliance certification such as SOC 2, ISO 27001, or PCI-DSS. North Tech Labs does not claim to hold any specific security certification, and nothing in this article should be read as an assertion that following it satisfies a regulatory, contractual, or certification requirement. Organizations with those requirements need a dedicated, independently verified process on top of sound engineering practice — the two are complementary, not interchangeable.

With that established, the rest of this guide covers the engineering practices themselves, in the order they tend to matter across a project's lifecycle: threat modeling before code is written, security review woven through the development process, dependency and supply-chain hygiene, authentication and session handling, authorization and least-privilege design, secrets management, input validation against injection-class vulnerabilities, and logging and monitoring for detection and response.

Threat modeling as a design-time practice

The single biggest structural mistake in how teams approach security is treating it as something applied to a system after it's built — a scan run before release, a checklist attached to a launch gate. By the time a system exists, its architecture has already made most of the decisions that determine how exploitable it is. Retrofitting security onto a finished design is possible, but it's consistently more expensive and less effective than designing with security considerations already in view.

Threat modeling is the practice of answering two questions deliberately, before implementation starts: what are we actually protecting, and who or what are we protecting it from? These sound simple, but teams frequently skip them because they seem obvious in the abstract and only turn out to be genuinely ambiguous once someone tries to answer them precisely for a specific system.

What you're protecting

Not all data and functionality carry the same risk if compromised. A system's threat model should identify, concretely:

  • What data does this system store or process, and which of it is sensitive — personal data, credentials, financial information, health information, proprietary business logic?
  • What actions can the system perform, and which of them would cause real harm if triggered by someone who shouldn't be able to trigger them — issuing a refund, deleting an account, changing a permission, sending a message on a user's behalf?
  • Where does data flow between components, and does it cross a trust boundary at any point — from an untrusted client to a server, from one internal service to another, from a third-party integration into your system?

Who you're protecting it from

The realistic threat actors for most business software are less exotic than headline-grabbing breaches suggest, and naming them concretely helps focus effort where it matters:

  • External attackers scanning broadly for common, automatable vulnerabilities rather than targeting a specific organization — this is the overwhelming majority of real-world exploitation attempts.
  • Authenticated users acting outside their intended scope — a legitimate account holder attempting to access another user's data or a permission level they weren't granted.
  • Compromised credentials or devices — a valid account being used by someone other than its legitimate owner, often the actual mechanism behind incidents that get reported as more sophisticated attacks.
  • Insiders, whether malicious or simply operating with more access than their role requires, which is why least-privilege design (covered later) matters even for trusted personnel.
  • Compromised dependencies or third-party integrations — increasingly common, and covered in depth below.

Turning a threat model into design decisions

The output of threat modeling shouldn't be a document that gets filed away — it should directly shape concrete choices: which components need authentication checks and at what granularity, which data flows need encryption in transit, which internal services should be reachable at all from the public internet, what the blast radius looks like if a specific component is compromised, and where a compromise in one part of the system should be prevented from cascading into another.

A useful working method is a short structured exercise per feature or per system boundary: draw the components and how data flows between them, mark each place a trust boundary is crossed, and for each boundary ask what could go wrong and what would happen if it did. This doesn't need to be elaborate to be effective — a whiteboard diagram and thirty focused minutes at design time catches more real issues than an exhaustive framework applied inconsistently or too late to change the architecture.

Threat modeling isn't a one-time exercise performed once at a project's inception either. It should recur whenever the system changes meaningfully: a new integration, a new user role, a new data flow touching something sensitive, or a new deployment environment. Treating it as a living practice tied to architectural change, rather than a document produced once and never revisited, is what keeps it useful over a system's life.

Secure SDLC: building review into the process, not just the release

A secure software development lifecycle embeds security consideration at every stage of building software, rather than concentrating it entirely into a pre-release scan or a separate security team's final sign-off. The earlier an issue is caught, the cheaper it is to fix — a design flaw caught in a design review costs a conversation; the same flaw caught after release costs a patch, a deployment, and potentially an incident response.

Design-stage review

Before implementation begins on anything that touches sensitive data, crosses a trust boundary, or introduces new authentication or authorization logic, a short design review focused specifically on security questions is worth the time it takes. Useful questions at this stage: What's the simplest way this could be misused? What happens if an input is malformed, missing, or hostile? What's the failure mode if a downstream dependency is unavailable or returns something unexpected — does it fail open or fail closed?

Code-review stage

Code review is one of the highest-leverage points in the entire lifecycle for catching security issues, because it happens on every change, by definition, if the team enforces it consistently — unlike a periodic audit that only samples a subset of the codebase at a point in time. Reviewers benefit from a short, consistent set of questions to bring specifically to security-sensitive changes:

  • Does this change introduce a new trust boundary, and if so, is input validated at that boundary?
  • Does this change touch authentication, session handling, or authorization logic, and if so, has it been reasoned through rather than just tested for the happy path?
  • Are any secrets, credentials, or sensitive values present in the diff, even temporarily or in a comment?
  • Does error handling leak more information than necessary — stack traces, internal paths, or database details — in a response a client could see?
  • Is a new dependency being introduced, and has it been evaluated (see below) rather than just added because it solved the immediate problem?

Automated checks as a baseline, not a replacement for judgment

Static analysis, dependency scanning, and secret-scanning tools integrated into a CI pipeline catch a meaningful share of common issues automatically and consistently, and they're worth adopting as a baseline for exactly that reason — they don't get tired, distracted, or rushed near a deadline the way a human reviewer sometimes does. But automated tooling has real limits: it's generally good at catching known patterns and much weaker at catching logic errors specific to a system's business rules, like an authorization check that's syntactically fine but conceptually wrong for the actual permission model. Automated checks and human review are complementary, not substitutes for each other.

Review before release is still necessary — just not sufficient on its own

None of this argues against a final security-focused review before a significant release. It argues against treating that final review as the only point security gets considered. A release-time review that catches nothing surprising because issues were already caught earlier is a sign the earlier stages are working, not evidence the final review was unnecessary.

Dependency and supply-chain hygiene

Modern applications are assembled from far more third-party code than first-party code, once every transitive dependency is counted — a single direct dependency can pull in dozens of others that a team never explicitly chose or reviewed. This makes the dependency tree itself a genuine attack surface, not a peripheral concern.

Automated dependency scanning

Continuous automated scanning for known vulnerabilities in dependencies, run as part of the CI pipeline rather than as an occasional manual check, is close to a baseline expectation at this point. It catches the case where a package you already depend on has a newly disclosed vulnerability — something no amount of careful review at adoption time would catch, since the vulnerability didn't exist yet when the dependency was added.

Pinning versions and controlling updates deliberately

Unpinned or loosely constrained dependency versions mean a build can pull in a different version of a package than the one that was tested, sometimes without anyone noticing until something breaks or, worse, until something silently changes behavior in a security-relevant way. Pinning versions and updating them through a deliberate, reviewed process — rather than automatically and silently — gives a team the chance to actually notice what changed. This doesn't mean freezing dependencies indefinitely, which trades one risk (unreviewed change) for another (accumulating known vulnerabilities in stale versions); it means treating updates as changes worth a look, not a silent background process.

Evaluating third-party packages before adopting them

Before adding a new dependency, a few minutes of evaluation reduces risk meaningfully: How actively maintained is it — recent commits, responsive maintainers, a reasonable release cadence? How large is its own dependency tree, since that tree becomes part of your attack surface too? Does it request more capability or access than the problem it solves actually requires? Is there a reasonably-sized, credible alternative already in use elsewhere in the codebase, avoiding an unnecessary new addition to the dependency graph for a problem already solved elsewhere in the project?

The real risk of transitive dependencies

The dependencies your direct dependencies bring in are often invisible unless specifically inspected, and they carry the same risk as anything added directly — a vulnerability or a compromised package several layers deep can affect an application that never knowingly imported anything unsafe. This is precisely the mechanism behind several widely reported supply-chain incidents: a compromise introduced into a small, widely-depended-upon package propagates outward through every project that depends on it, directly or transitively, often without any of those downstream projects' maintainers being aware the dependency existed in their tree at all. Tooling that visualizes and scans the full dependency tree, not just direct dependencies, is worth the setup effort specifically because of this.

Authentication and session security fundamentals

Authentication determines who a system believes a user is; getting the fundamentals wrong undermines every authorization decision built on top of it, no matter how carefully those decisions are designed.

Password handling

Passwords should never be stored in plaintext or with reversible encryption — they should be hashed with an algorithm specifically designed for password storage, using a per-user salt and a deliberately expensive work factor tuned to make brute-force attempts costly even against a stolen hash database. General-purpose fast hash functions designed for speed — the kind used for checksums or general hashing elsewhere — are the wrong tool here precisely because their speed makes brute-forcing cheaper, not because they're broken in some other sense.

Session token handling

Once a user authenticates, how the resulting session is handled matters as much as the login itself. Session tokens should be generated with sufficient randomness to make guessing infeasible, transmitted only over encrypted connections, and stored on the client in a way that limits exposure to client-side script access where the platform supports it. Sessions should have a reasonable expiry rather than living indefinitely, and there should be a real mechanism to revoke a session — if a device is lost, a credential is suspected compromised, or a user explicitly logs out everywhere. A session model with no expiry and no revocation path turns a single leaked token into a standing, unlimited-duration compromise.

Multi-factor authentication as a meaningfully higher bar

Password-only authentication, however well the password itself is hashed and stored, remains vulnerable to the ways passwords actually get compromised in practice — reuse across services, phishing, and credential-stuffing against leaked password lists from unrelated breaches. Multi-factor authentication, requiring a second independent factor beyond the password, raises the bar meaningfully because it means a leaked or guessed password alone is no longer sufficient to gain access. It's worth treating MFA as a default expectation for anything protecting sensitive data or consequential actions, not an optional extra reserved for high-security contexts alone.

Account recovery deserves the same scrutiny as login

Account recovery flows — password reset, account unlock — are a common weak point precisely because they're often designed with less scrutiny than the primary login path, while functionally providing an alternative way into the same account. A recovery flow should be held to the same security standard as login itself: time-limited and single-use reset tokens, no recovery mechanism that leaks whether an account exists at all through a difference in response, and notification to the account owner when a recovery flow is used.

Authorization and least-privilege design

Authentication answers who someone is; authorization answers what they're allowed to do — and conflating the two, or assuming that a successfully authenticated user is automatically entitled to any action they can technically request, is a recurring source of real vulnerabilities.

Least privilege as a default, not an exception

The principle of least privilege means every user, service, and component should have only the access genuinely required for its function, and nothing more, by default. This applies as much to a backend service calling another internal service as it does to an individual user's account permissions. A service account with broad database access because it was simpler to configure that way at the time is a common, avoidable source of unnecessary blast radius if that service is ever compromised.

Avoiding ambient authority

Ambient authority describes a situation where access is available simply because of where a request originates, or because a session exists, rather than being explicitly checked against what that specific request should be allowed to do. A common concrete form of this: an internal API that trusts any request coming from inside the internal network, with no per-request authorization check, on the assumption that anything inside the perimeter is automatically trustworthy. That assumption fails the moment any single internal component is compromised, at which point the lack of internal authorization checks turns one compromised service into unrestricted lateral access across everything else inside the perimeter.

Scoping permissions narrowly and explicitly

Permission systems should be designed around explicit, narrowly-scoped grants rather than broad roles that happen to include more access than most assignees actually need. It's worth auditing periodically whether granted permissions still match actual need — access accumulates over time as people change roles or projects, and rarely gets proactively reduced without a deliberate review process to catch it.

Checking authorization at the point of action, not just at the point of entry

A user being authorized to view a list of resources doesn't automatically mean they're authorized to act on every individual resource within it. Authorization checks need to happen at the point where a specific action is performed on a specific resource, not only at a coarser entry point like "is this user logged in" or "does this user have access to this general area of the application." This is precisely the class of vulnerability behind many real-world incidents where a user could access another user's data simply by changing an identifier in a request — the entry-point check passed, but no per-resource check existed behind it.

Secrets management

Secrets — API keys, database credentials, signing keys, third-party service tokens — require handling that's deliberately different from ordinary configuration, because their exposure has direct and often immediate consequences.

Never commit secrets to source control

This is a simple rule that's violated constantly in practice, usually accidentally rather than deliberately — a credential hardcoded during local testing and never removed before a commit, a configuration file with real values checked in as a convenience. Once a secret reaches a version control history, removing it from the current version of a file doesn't remove it from history, and the only fully reliable remediation is treating the secret as compromised and rotating it. Automated secret-scanning in CI, configured to block a commit or pull request containing a detected secret pattern, is a low-cost control that catches this class of mistake before it reaches a shared repository at all.

Use a dedicated secrets manager

Secrets belong in a purpose-built secrets manager or vault, not in environment files checked into a repository, not in plaintext configuration on a server, and not passed around informally through chat or documents. A dedicated secrets manager provides access control specific to secrets (distinct from general infrastructure access), an audit trail of what was accessed and when, and a controlled mechanism for rotation that doesn't require manually chasing down every place a value is used.

Rotating credentials

Secrets should be rotated on a reasonable schedule and immediately whenever a compromise is suspected, whenever an employee or contractor with access to a given secret changes role or leaves, or after a scanning tool detects an exposed value in the wrong place. A secret that has never been rotated since a system's initial setup, sometimes for years, represents a standing risk disproportionate to how little effort rotation actually requires when it's built in as a routine practice rather than treated as an exceptional event.

Input validation and injection-class vulnerabilities

A large share of the most damaging and most common vulnerability classes — SQL injection, cross-site scripting, and their relatives — share a root cause: data crossing a trust boundary gets treated as trusted, or gets interpreted as code or a command when it should have been treated strictly as data.

SQL injection and the case for parameterized queries

SQL injection occurs when untrusted input is concatenated directly into a database query, allowing an attacker to alter the query's structure rather than just supply a value within it. The reliable fix is not attempting to sanitize or escape every possible malicious pattern — that approach is fragile because new patterns keep appearing — but using parameterized queries or a properly configured query-building layer that keeps data structurally separate from the query itself, so that user-supplied input can never be interpreted as part of the query's logic no matter what it contains.

Cross-site scripting and output encoding

Cross-site scripting occurs when untrusted input is rendered into a page in a way that allows it to execute as script in another user's browser. The core defense is encoding output appropriately for the context it's rendered into — HTML, an attribute, a URL, a script context each require different encoding — combined with a strict content security policy that limits what a page is allowed to execute even if an encoding gap slips through somewhere.

Why validating and escaping at the boundary beats sanitizing everywhere

A recurring, costly mistake is trying to sanitize data at every single point it might later be used, scattered throughout a codebase, rather than validating strictly at the point it enters the system and encoding correctly at the point it's output. Sanitizing everywhere is both incomplete — it's genuinely difficult to guarantee every use site was covered, especially as a codebase grows and changes — and inconsistent, since different developers make different judgment calls about what "sanitized" means in a given spot. Establishing a clear boundary where input is validated against an explicit expected shape and rejected if it doesn't match, and a corresponding discipline of encoding correctly for context at output time, produces a system that's far more reliably defended than one relying on sanitization calls sprinkled wherever someone remembered to add one.

A short framework for where checks belong

Trust boundaryWhat to checkWhere the check belongs
Client input reaching a serverType, format, length, and allowed value rangeAt the API boundary, before the value is used anywhere downstream
Data entering a database queryStructural separation of data from query logicAt the query-construction layer, via parameterization
Data rendered into a pageCorrect encoding for the output contextAt the point of rendering, not earlier
Data received from a third-party integration or webhookSignature/origin verification and schema validationBefore the payload is trusted or processed at all
Internal service-to-service callsAuthentication and authorization, not just network locationAt each service boundary, regardless of network trust assumptions

Treating every row in that table as a distinct, deliberate checkpoint — rather than assuming a check performed at one boundary covers the others — closes the gap that injection-class vulnerabilities most often exploit.

Logging and monitoring for incident response

Prevention reduces the likelihood of an incident; detection determines how quickly one is noticed and how much damage accumulates before it's addressed. Both matter, and a security posture built entirely around prevention with no meaningful detection capability tends to discover a problem only when the consequences become impossible to miss — usually much later, and much more expensively, than a well-instrumented system would have.

What's worth logging

Authentication events (successful and failed login attempts, password resets, MFA enrollment changes), authorization failures (a user attempting an action or accessing a resource they weren't permitted to), administrative actions (permission changes, configuration changes, data exports), and anomalous patterns (a sudden spike in failed requests, access from an unexpected pattern of locations or times) are all genuinely useful signal for detecting misuse in progress rather than only reconstructing it after the fact.

What should never be logged

Raw secrets — passwords, API keys, session tokens, private keys — should never appear in logs in plaintext, and neither should full payment card numbers, government identification numbers, or other data specifically restricted under an applicable compliance regime. If a log entry needs to reference that a sensitive action occurred, it should reference an identifier or a masked value, never the raw sensitive value itself. This matters because logging infrastructure is frequently less tightly access-controlled than the primary application and database, retained for longer than the operational data it describes, and searched by a wider set of people — including during incident response, when speed sometimes comes at the expense of the same care that would normally apply to a production data query. Logging a secret in the clear effectively creates a second, less-guarded copy of it.

Detection matters as much as prevention

An effective monitoring setup includes alerting on the events that matter most — repeated authentication failures against a single account, authorization failures at unusual volume or against unusual resources, changes to security-relevant configuration — routed to someone who will actually act on them, not just recorded somewhere that gets reviewed occasionally after the fact. It's worth periodically asking, as a team, whether an incident of a specific plausible kind (a compromised credential, a misconfigured permission, an exposed endpoint) would actually be noticed by current logging and alerting, and how quickly. If the honest answer is "not until a user reports something going wrong," that's a concrete gap worth closing before it's tested by a real incident rather than a hypothetical one.

Having a response plan before it's needed

Detection is only useful if it's paired with a plan for what happens next — who gets notified, what gets investigated first, how a compromised credential or session gets revoked quickly, and how affected users or stakeholders get informed if that becomes necessary. Working out these steps during an actual incident, under time pressure and with incomplete information, produces worse decisions than working them out in advance during a calm moment when they can be reasoned through properly.

Security as an ongoing practice, not a one-time project

Every practice covered here — threat modeling, secure-SDLC review, dependency hygiene, authentication and session handling, least-privilege authorization, secrets management, input validation, and logging for detection — has one thing in common: none of them are ever finished in a durable sense. New dependencies get added and need evaluating. New features introduce new trust boundaries that need threat modeling. New team members need onboarding into review practices. Previously secure configurations become outdated as new vulnerability classes and attack techniques emerge. Access that was appropriately scoped when granted drifts out of alignment with actual need over time.

Treating security as a project with a defined end — a hardening sprint before a launch, a checklist completed once — misrepresents how risk actually behaves over a system's life. It's better understood as an ongoing engineering discipline and a recurring budget line: time allocated for dependency updates, recurring review of access and permissions, periodic revisiting of the threat model as the system evolves, and sustained attention in code review rather than a one-time investment assumed to hold indefinitely. Teams that budget for this as continuous, ordinary engineering work tend to have meaningfully fewer serious incidents than teams that treat security as a phase they pass through once and move on from.

None of this replaces the value of independent verification. Internal engineering discipline and external assessment — a licensed audit, a penetration test conducted by a qualified third party, or a formal certification process — serve different purposes and neither substitutes for the other. The practices in this guide are the foundation that makes external assessment more likely to go well; they are not themselves that assessment, and no engineering team should represent internal practice as equivalent to independently verified security assurance.

Frequently asked questions

Is following this guide the same as passing a security audit?

No, and this is worth being explicit about. This guide describes sound engineering practices that reduce risk when applied consistently, but it is not a substitute for a licensed security audit, an independent penetration test, or a formal compliance certification. Audits and certifications involve structured, independently verified assessment against a specific standard, often by an accredited third party, and typically produce evidence you can show to customers, regulators, or partners. Good engineering practice is a necessary foundation for passing that kind of assessment, but it isn't a replacement for it. If your organization needs a certification such as SOC 2 or ISO 27001, or needs to satisfy a regulatory requirement, that requires a dedicated process with qualified auditors, not just adherence to a practices guide.

When should threat modeling happen in a project?

As early as possible, ideally before any significant code is written and again whenever the design changes meaningfully — a new integration, a new user role, a new data flow that touches sensitive information. Threat modeling done only after a system is built tends to surface issues that are expensive to fix because the architecture already assumes a particular trust structure. Done at design time, it costs little more than a focused conversation and a diagram, and it directly shapes decisions like where authentication checks live, what data actually needs to be collected, and which components should never be allowed to talk to each other directly.

What's the single most common authentication mistake teams make?

Treating password-only authentication as sufficient for anything that protects meaningful data or actions, and not offering or requiring multi-factor authentication where it matters. A close second is mishandling sessions after login succeeds — long-lived tokens with no expiry, tokens stored where client-side scripts can read them, or no way to revoke a session if a device is lost or a credential is suspected compromised. Getting the login form right is necessary but not sufficient; what happens to the session afterward matters just as much.

Do small engineering teams really need to worry about supply-chain risk from dependencies?

Yes, and arguably more than larger teams, because small teams often have less capacity to manually review every package they pull in. Most applications today are assembled from far more third-party code than first-party code once transitive dependencies are counted, and a vulnerability or a deliberately malicious update several layers deep in that dependency tree can compromise an application that never knowingly imported anything unsafe. Automated dependency scanning and deliberate version pinning are inexpensive relative to the risk they address, and they scale down to small teams just as well as large ones.

What should never appear in application logs?

Raw secrets — passwords, API keys, session tokens, private keys — should never be written to logs in plaintext, and neither should full payment card numbers, government identification numbers, or other data a compliance regime specifically restricts. If a log needs to reference that an action involving sensitive data occurred, reference it by an identifier or a masked/truncated form, not the raw value. Logging systems are often less tightly access-controlled than the primary application and database, are retained for longer, and are searched by more people during incident response — so anything sensitive logged in the clear effectively creates a second, less-guarded copy of that data.

Have a specific question about your project?

This article covers the general pattern — the fastest way to get a specific answer is to ask us directly.