Guides

A practical guide to secure app development

Secure app development means deciding what you must protect, designing so the common attacks cannot work, and checking the result before every release. This guide follows the whole lifecycle of a web or mobile app, from threat modelling to launch, with decisions and checklists a team can use as written.

22 min read

Start a project

Who this guide is for and how to use it

This guide is for founders, product owners, engineering leads and developers who are building or commissioning a web or mobile app and want security built in, not bolted on at the end. You do not need a security background. Where a topic runs deep, we name the technique and say what to do next.

Read it in lifecycle order, or jump to the chapter that fits where you are. Early in design, start with threats and architecture. Mid-build, read the chapters on authentication, input and data. Before launch, go to testing, mobile security and the checklist. Once live, focus on third-party code and incident readiness. Most chapters end with items you can paste into your tracker.

The method is the one we apply to the products we build: every project begins with a threat review, we follow secure coding practice and test before every release, and a security review before launch produces a prioritised fix list. See how we work and the security practices behind it, then use this guide to hold any team, your own included, to the same standard.

Security is a set of trade-offs, not a state you reach. The aim is to make attacks expensive, failures visible and recovery quick. This is general engineering guidance, not legal advice: where a law or standard applies to your product, confirm the details with qualified counsel.

Assets, threats and security requirements

Security effort is limited, so spend it where harm would be greatest. Start with an honest list of what you protect and who might come for it.

List what you are protecting

  • User accounts, and the passwords, tokens and recovery paths that open them.
  • Personal and financial data, and anything a law or contract treats as sensitive.
  • Money movement: payments, payouts, refunds and balances.
  • Admin and support tools, which are often more powerful than the product itself.
  • Source code, signing keys, cloud accounts and the pipeline that deploys the app.

Name who might attack

Threats come from a short list: automated scanners that probe everything online, criminals after money or data, users abusing access they already have, compromised partner or insider accounts, and occasionally a targeted attacker. Most apps meet the first three every day, so plan for them first.

Run a lightweight threat model

Threat modelling is a structured conversation about what could go wrong, held while the design can still change. A small team can run a useful session in an hour or two for each core flow, as our article on threat modelling for small teams shows.

  1. Draw the flow on one page: users, app, API, database, third parties and the trust boundaries, where data passes from less trusted to more trusted.
  2. At each boundary, ask what an attacker could spoof, tamper with, leak, disrupt or abuse. The STRIDE prompts (spoofing, tampering, repudiation, information disclosure, denial of service, elevation of privilege) work as a checklist.
  3. Rank findings by impact and ease of attack, then turn each accepted one into a ticket with an owner and a test.
  4. Review the model when you add a major feature, integration or type of data.

Turn threats into testable requirements

A requirement is a sentence you can test. The app must be secure cannot be tested; a user can read only their own invoices can. The OWASP Application Security Verification Standard (ASVS) offers ready-made requirements at three levels, so pick the level that matches your risk. Use the OWASP Top 10 as an awareness list, not a specification.

ThreatRequirementHow you test it
A user changes an ID and reads another user's order.Every request checks access to that specific record.Automated test with two accounts.
Passwords stolen elsewhere are tried on your login.Rate limiting, breached-password checks, optional multi-factor sign-in.Abuse test on staging.
A dependency with a known flaw reaches production.The pipeline blocks builds with unresolved critical findings.Plant a known-bad package and watch the build fail.

Threat model before the design is final: changing a diagram is cheap, changing a shipped data model is not. If you want a facilitator, see our threat modelling service.

Architecture and design choices

The cheapest security fixes are decisions made before the code exists. The OWASP Top 10 lists insecure design as its own category for this reason: careful coding cannot repair a flow that was unsafe by construction.

Principles worth putting on the wall

  • Least privilege. Each user, service and token gets only the access its job needs.
  • Deny by default. Access is refused unless a rule allows it, and new endpoints start closed.
  • Defence in depth. Assume one control fails and put another behind it, such as validation plus parameterised queries.
  • Small attack surface. Fewer endpoints, features, dependencies and stored data mean less to defend.
  • Fail safely. When something breaks, the app refuses access and hides its internals.
  • No secrets in clients. Anything shipped to a browser or phone can be read by whoever holds it.

Choices that remove whole classes of bugs

ChoiceWhat it prevents or limits
Use a mature framework's built-in protections: output escaping, anti-forgery, ORM parameterisation.Most cross-site scripting, forgery and basic injection bugs, unless someone bypasses them.
Use a managed identity provider or a well-reviewed authentication library.Weak password storage and broken sessions.
Separate the admin interface, with its own network rules and stronger sign-in.A flaw in the public app turning into admin power.
Isolate tenants in the data layer, for example with PostgreSQL row-level security.One customer reading another's data.
Keep databases and internal services on private networks.Direct attacks from the internet.
Treat the API as the real product and the web and mobile apps as its clients.Business rules enforced only in the interface.

Decide the abusable flows early

Some flows deserve extra design time because abuse pays: sign-up, login, password reset, payment, upload, export and every admin action. For each, write down how it could be abused at scale and what limit you will set: rate limits, quotas, step-up checks or approval for large actions.

Cloud setup is architecture too. On AWS, Azure or Google Cloud the principles match even where product names differ: separate accounts or projects for each environment, least-privilege roles, private storage by default, and infrastructure defined as code and reviewed like application code.

Authentication, sessions and authorisation

Authentication proves who someone is. Authorisation decides what they may do. Broken versions of both sit behind many real breaches, so give them design time, tests and review.

Passwords and sign-in

  • Prefer a proven identity provider or library. If you store passwords, use a slow, salted hash built for the purpose: Argon2id first, or scrypt, bcrypt or PBKDF2. Never a fast hash alone, and never reversible encryption.
  • Follow current guidance such as NIST SP 800-63B: allow long passphrases, check new passwords against known breached ones, and avoid arbitrary composition rules and forced periodic changes.
  • Limit attempts per account and per source, and never reveal in an error or reset response whether an account exists.
  • Offer multi-factor authentication and require it for administrators and sensitive actions. Prefer passkeys, authenticator apps and hardware keys to SMS codes, which are weaker against SIM swapping and phishing.
  • Treat recovery as part of authentication, since it is often the weakest way in: single-use, time-limited links, notification of changes and re-authentication before an email or password change.

Sessions and tokens

  • Issue a fresh session identifier at sign-in and whenever privilege changes, to prevent session fixation.
  • For cookie sessions, set HttpOnly, Secure and a suitable SameSite attribute, with idle and absolute timeouts.
  • Make logout and password change end sessions on the server, not only in the client.
  • With JSON Web Tokens, verify the signature and algorithm on the server, check expiry, issuer and audience, keep them short-lived and put no secrets inside: they are signed, not hidden.
  • For mobile and single-page apps using OAuth 2.0 and OpenID Connect, use the authorisation code flow with PKCE in the system browser, not an embedded web view or the implicit flow.

Authorisation: check every request

Broken access control is consistently near the top of the OWASP Top 10. The typical bug is simple: the app checks that you are signed in, but not that this record is yours.

  • Enforce authorisation on the server for every request. Hiding a button is not access control.
  • Check object-level access: for every record ID in a request, confirm the caller may touch it. This is the insecure direct object reference, called broken object-level authorisation in API security.
  • Check function-level access: admin and support actions need explicit role checks, not an unlisted URL.
  • Read identity and role from the session or token, never from fields the client sends.
  • Block mass assignment: accept an explicit list of fields so a user cannot set isAdmin or a price.
  • Centralise policy in one layer so rules are written and tested once. In multi-tenant systems, put the tenant in every query.

The best authorisation test is boring and effective: sign in as two ordinary users and as an admin, then try every endpoint with the other person's IDs. Automate it so it runs on every build.

Injection and input handling

Injection happens when untrusted input is treated as code or as part of a command. It remains on the OWASP Top 10 because the cause is so common: a string built by concatenation and handed to an interpreter such as a database, a shell or a browser. Two habits prevent most of it. Keep data and code apart, and validate input at the edge.

SQL injection

SQL injection lets an attacker change a database query by sending text that the app pastes into it. The fix is old and reliable: parameterised queries send the query and the values separately, so a value can never become part of the query.

  • Use parameters for every value, in raw SQL and in the escape hatches of an ORM. A query builder protects you only while you stay inside it.
  • Parameters cannot replace table or column names. When a sort field comes from the user, map it through a fixed allow-list.
  • Give the application's database account only the rights it needs, so a flaw does less damage.
  • Show users a generic error and keep database messages in the server logs. The same rule holds for NoSQL stores, LDAP and search engines: user input stays data.

Cross-site scripting

Cross-site scripting (XSS) runs an attacker's script inside your page, where it can act as the signed-in user. It can be stored, reflected in a response, or DOM-based, where client-side code writes untrusted data into the page.

  • Encode output for its context. HTML text, attributes, scripts and URLs each need different escaping, so rely on a framework that does it by default.
  • Treat the escape hatches as warning signs in review: dangerouslySetInnerHTML in React, v-html in Vue, bypassSecurityTrust in Angular and plain innerHTML.
  • Allow only http, https and mailto in links built from user data, and refuse javascript: addresses.
  • If users submit rich text, clean it with a maintained HTML sanitiser and a short list of allowed tags. Keep session cookies HttpOnly so injected script cannot read them.

Content security policy

A content security policy (CSP) is a response header that tells the browser which scripts, styles, frames and connections a page may use. If an injection slips through, a good policy can stop the attacker's script from running. It is a second layer behind output encoding, never a replacement.

  1. Start in report-only mode (the Content-Security-Policy-Report-Only header) and collect reports to learn what your pages really load.
  2. Begin from a strict base: default-src 'self', object-src 'none', base-uri 'self', and frame-ancestors to control who may embed you.
  3. Allow your own scripts with nonces or hashes instead of unsafe-inline, and add third-party origins one at a time, each with a reason.
  4. Switch to enforcing mode, and keep reporting on so new breakage shows up.

Other injection and input risks

RiskWhat goes wrongDefence
Command injectionUser input reaches a shell command.Avoid the shell. Pass arguments as an array, never as one joined string.
Path traversalA name such as ../../secrets escapes the intended folder.Generate file names yourself, then resolve and check the final path.
Server-side request forgeryThe server is tricked into fetching internal addresses, such as a cloud metadata service.Allow-list outbound destinations and block internal ranges.
Unsafe file uploadA disguised script or an oversized file is stored and served.Check type by content, cap the size, rename files and serve them from a separate domain.
Unsafe deserialisationCrafted serialised data runs code.Prefer plain JSON checked against a schema.

Validate at the edge

Validate every input on the server, whatever the client checked. Prefer allow-lists to block-lists: define what a valid value looks like (type, length, range and format) and reject the rest. Share one schema between web, mobile and API so they agree. Validation does not replace parameterised queries and output encoding: a valid name such as O'Brien contains an apostrophe, and it is dangerous in the wrong context.

Secrets and data protection

Secrets are the keys to everything else: database passwords, API keys, signing keys and tokens. Data protection decides what an attacker gets if one layer fails. Treat both as design topics, not settings you tidy before launch.

Keep secrets out of code and clients

  • Never commit secrets to a repository, even a private one. Turn on secret scanning and push protection.
  • Load secrets at run time from the platform's secret store or a manager such as AWS Secrets Manager, Azure Key Vault or Google Secret Manager. Keep them out of images, logs and error messages.
  • Use a separate secret for each environment and service, with the narrowest permissions that work, and prefer short-lived credentials.
  • Assume anything in a browser bundle or mobile binary is public. In Next.js, for example, variables prefixed with NEXT_PUBLIC_ are sent to the browser.
  • When a secret leaks, revoke it first and clean up second: deleting a commit does not un-leak a key.

Protect data through its life

Collect only what you need, keep it only as long as you need it, and know where it lives.

  • Encrypt in transit with TLS everywhere, including between internal services, and send a Strict-Transport-Security header.
  • Encrypt at rest with the managed encryption of your database, storage and backups. For the most sensitive fields, add application-level encryption with keys held in a key management service.
  • Use well-reviewed cryptography libraries. Do not invent algorithms, and do not use MD5 or SHA-1 for security purposes.
  • Keep personal data and secrets out of logs and error reports, and use fake data in development and test environments.
  • Encrypt backups, limit who can read them and test a restore.
  • Let a payment provider's hosted fields or checkout take card numbers, so they never reach your servers. This also narrows the card-security rules that apply to you; confirm the details with your provider.

Personal data laws in the Gulf, the European Union and the United States differ in scope, in consent rules and in what they say about storing or moving data across borders. This guide is awareness only: confirm what applies to your product with qualified counsel. See also our guide to launching software in the Gulf.

Dependencies and the software supply chain

Most of a modern app is code you did not write: frameworks, packages, SDKs, container images and build tools. Each is a trust decision. Trouble comes in three kinds: a known flaw in a version you run, a malicious or hijacked package, and a compromised build pipeline.

Control what you pull in

  • Add a dependency on purpose. Check that it is maintained and widely reviewed, and prefer a few well-known libraries to many small ones.
  • Commit lockfiles and install from them in the pipeline (with npm, that means npm ci), so the build uses the versions you reviewed.
  • Watch for lookalike package names (typosquatting). Keep internal packages in a private, scoped registry so that a public package with the same name cannot replace them (dependency confusion).
  • Turn on multi-factor authentication for every registry, GitHub and cloud account that can publish or deploy.

Find known flaws and fix them steadily

Run dependency scanning on every pull request and on a schedule, because new flaws are published for code you shipped long ago. Rank findings by whether the vulnerable code is reachable and whether the app faces the internet, not by score alone.

Update in small, frequent batches: a team that updates every week handles an urgent patch calmly, while a team that has not updated in a year faces a risky leap. Keep a software bill of materials (SBOM), in a format such as CycloneDX or SPDX, so you can tell quickly whether a newly announced flaw affects you.

Harden the pipeline

Your CI/CD pipeline can deploy to production, so attackers target it directly.

  • Protect the main branch: required reviews, passing checks and no direct pushes.
  • Give each job the least permission it needs, and keep deploy credentials away from jobs that run code from forks.
  • Pin third-party pipeline actions and base images to a specific version or digest, not a moving tag.

Treat third-party scripts in the browser with the same care. A chat widget or tag manager runs with the full power of your page, so load as few as you can, restrict them with your CSP and keep them off pages that handle passwords or payments.

Mobile app security

A mobile app runs on a device you do not control, held by someone who might be an attacker. They can unpack the app, read its files, run it on a modified phone and watch its network traffic. Treat the app as an untrusted client of your server.

The reference standard is the OWASP Mobile Application Security Verification Standard (MASVS), with the Mobile Application Security Testing Guide (MASTG) explaining how to test each item. A native app and a cross-platform app built with Flutter or React Native face the same threats; the framework changes how the code is packaged, not what an attacker wants. Choosing between them is a separate decision, covered in native or cross-platform.

What to do on the device

AreaDoAvoid
StorageKeep tokens and keys in the iOS Keychain or the Android Keystore.Plain files, preferences, logs and backups that contain secrets.
NetworkAllow TLS only. For high-risk apps consider certificate pinning, with a backup pin and a way to update it.Cleartext traffic, or disabled certificate checks left in a release build.
Sign-inUse the system browser with OAuth 2.0 and PKCE. Use biometrics to unlock a key, not as the only proof sent to your server.Embedded web views for sign-in, and passwords stored on the device.
Links and web viewsUse verified universal links and app links, treat deep link data as untrusted input and limit web views to allow-listed origins.Custom URL schemes for sensitive actions, and native bridges open to remote content.
SecretsKeep API secrets on the server and give the app a short-lived, narrowly scoped token.Keys in the binary, in resource files or in the JavaScript or Dart bundle.

The server must not trust the app

Obfuscation, root and jailbreak detection, and attestation services such as the Play Integrity API and Apple App Attest raise the attacker's effort and give your server a signal. None is proof, because the attacker controls the device. Whatever the app reports, the server still authenticates every request, checks authorisation and limits abuse.

Plan for updates

Old app versions stay in the field for months. Give your API a minimum supported version and an in-app prompt to update, so a critical fix can reach users. Keep third-party SDKs current and make sure your store privacy declarations match what the app really does.

Testing: review, automation and penetration tests

No single method finds everything. Layer them: people catch design and logic flaws, tools catch known patterns at speed, and an independent tester attacks the whole system as a real attacker would.

Code review with a security lens

Add security questions to your review template. Does this endpoint check who the caller is and which record they may touch? Is every query parameterised? Does the change add a secret, a dependency or a new kind of data? For high-risk areas such as authentication, payments, file handling and admin tools, ask a reviewer with security training to approve. A dedicated secure code review of those areas before launch finds what everyday review misses; our secure code review service is one way to get it.

Automated checks in the pipeline

CheckWhat it findsWhat it misses
Static analysis (SAST)Risky patterns in source code, such as string-built queries.Logic flaws and run-time problems. Expect false positives to tune.
Dependency scanningKnown flaws in packages and images.Unreported flaws and malicious code nobody has flagged yet.
Secret scanningKeys and tokens in code and history.Secrets kept elsewhere, such as in chat or tickets.
Dynamic testing (DAST)Missing headers, injection and misconfiguration in a running app.Flaws behind complex sign-in or business rules.
Security tests you writeRegressions in your own rules, such as access to another user's record.Anything nobody wrote a test for.

Make the important checks block the merge and send the rest to a visible backlog. A scanner nobody reads is noise.

Penetration testing

A penetration test is a time-boxed, authorised attack by a skilled person on your running app. A vulnerability scan lists known issues automatically, while a tester also chains weaknesses and abuses business logic, as our article on penetration tests and vulnerability scans explains. Plan one before launch, after major changes and regularly for apps that hold sensitive data.

  • Agree the scope, the test window and the rules of engagement in writing.
  • Test a production-like environment with fake data, and give the testers an account for every role.
  • Ask for reproducible findings, severity judged in your context and fix advice, not just a scanner export.
  • Budget time to fix the findings and ask for a retest of the serious ones.

If you want an outside tester, see our penetration testing service.

Release gates and handling vulnerabilities

A release gate is a short list of conditions that must be true before code reaches users. Write it down, automate what you can, and let a named person approve any exception with a reason and an expiry date.

A release gate worth having

  • All tests pass, including the security tests for access control and input handling.
  • No unresolved critical or high findings from scans, reviews or penetration tests, or a recorded exception for each.
  • Secret scanning is clean, and production uses secrets that staging never sees.
  • Production is hardened: debug modes and detailed error pages off, default accounts removed, admin tools closed to the public internet and CORS limited to known origins.
  • Security headers are set: Strict-Transport-Security, Content-Security-Policy with frame-ancestors, X-Content-Type-Options and Referrer-Policy.
  • Logging and alerts work, a restore has been tested and a rollback plan exists. For mobile, release builds are signed with protected keys and debugging is off.

Log, watch and be ready

You cannot respond to what you cannot see. Log sign-ins and failures, password and email changes, permission denials and admin actions. Store logs centrally, protect them from tampering and keep personal data and secrets out of them. Alert on patterns such as bursts of failed sign-ins or one account reading many records.

Write a short incident plan before you need one: who decides, how to revoke keys and sessions, where the logs are and how to inform users. Duties to notify regulators or users after a breach differ by law and by sector, so confirm them with qualified counsel in advance, not during the incident.

Take reports and fix vulnerabilities

Someone outside your team will eventually find a flaw, so give them a safe way to tell you. Publish a vulnerability disclosure policy that covers scope, what you ask of reporters and what they can expect from you; our own vulnerability disclosure policy is built that way. Many teams also publish a security.txt file (RFC 9116) so researchers can find the contact quickly.

  1. Acknowledge the report and reproduce the problem.
  2. Rate severity in your context: who can exploit it and what they gain. A score such as CVSS helps, but it is an input, not the answer.
  3. Assign an owner and a target date by severity.
  4. Fix the cause, not only the symptom, and add a test that fails if the flaw returns.
  5. Release, tell affected users where needed and hold a blameless review: which gate should have caught it?

A secure app checklist you can copy

Use this as a starting list for your tracker, and cut or add items to fit your product. Each line is a yes or no question.

Design

  • Do we have a written list of assets and a threat model for each core flow?
  • Are security requirements written as statements we can test?

Build

  • Is every query parameterised and every output encoded for its context?
  • Does every endpoint check who the caller is and which record they may touch?
  • Are secrets kept out of code and clients?

Verify

  • Do dependency, static and secret scans run on every pull request?
  • Has an independent penetration test been done, with the serious findings retested?

Release and operate

  • Is the release gate written down and automated where possible?
  • Are security events logged and monitored, with alerts that reach a person?
  • Do we have an incident plan, a tested restore and a published way to report vulnerabilities?

Security work never ends, but it gets cheaper when it is part of every sprint instead of a rescue before launch. If you want a second pair of eyes, our cybersecurity service covers threat modelling, secure code review and penetration testing.

Questions we get asked

  • Before the design is final. A threat model of the core flows, held while the diagrams can still change, is the cheapest security work you can do, because it removes whole classes of flaws before any code exists. After that, security continues in every sprint through code review, automated checks and a release gate, not as one audit at the end.

  • Usually yes for an app that handles accounts, money or personal data. Scanners find known patterns quickly, so keep them running. A human tester finds what they miss: broken access control across roles, flawed business logic and chains of small weaknesses. Scan continuously, and book a penetration test before launch and after major changes.

  • Neither is safer by default, because the risks differ. A web app runs on your servers and in the user's browser, so most attacks arrive over the network. A mobile app also runs on a device the attacker may control, so they can inspect its binary, files and traffic. The practical rule: keep no secrets in a mobile client, and let the server make every decision.

  • In the platform's secret store or a dedicated secrets manager, loaded at run time, with a separate secret for each environment and service. Never keep them in source code, in a browser bundle or inside a mobile app, because anything shipped to a client can be read. Turn on secret scanning, rotate on a schedule and revoke at once if one leaks.

  • Continuously, in small batches, rather than once a year. Scan every pull request and on a schedule, because new flaws appear in code you already shipped. Apply urgent fixes for reachable, internet-facing components as soon as you can test them, and keep a bill of materials so you can tell quickly whether a newly announced flaw affects you.

Keep exploring

Ready to build something secure?

Tell us about your product. We will reply with a clear plan.

Start a project

We would like to use Microsoft Clarity to see how visitors use the site, so we can improve it. It sets cookies and is off unless you accept. Cookie policy