The OWASP Top 10, for Vue developers
Five habits that prevent most web security bugs, then the OWASP Top 10:2025 mapped onto the places in a Nuxt app where each risk lives.
What you'll learn
When you finish, you can do these.- 1Follow five habits that prevent most security bugs
- 2Find where each OWASP Top 10 risk lives in a Nuxt app
- 3Choose the right OWASP document to design, release or fix
- 4Spot and fix a route that leaks orders, keys and errors
The Corner Shop, the small online shop this track keeps safe, is a Nuxt app: Vue with its own server. Ana, a customer, reads her order from an API route, a server address that returns data: /api/orders/1042. She changes the number to 1043 and reads the order of Ben, another customer. Nothing breaks: a security bug works for everyone, including the person who should not see it.
This track teaches you to catch such bugs before an attacker does, starting with five habits and the OWASP Top 10, the list most teams use to name them. The Code tab holds Ana's order route, fixed; by the end you can say which risk each of its checks stops. You need one idea from Your first component, the Beginner track's first lesson: double braces in a template, such as {{ name }}, print a value as plain text, never as HTML.
Five habits that prevent most of the list
Five habits prevent most of the bugs on the list. Each lesson in this track takes one further.
- Escape by default. Vue prints
{{ review }}, a customer's review, as plain text, so a<script>tag inside it shows as characters and never runs. That is escaping. Keep it that way: everyv-html, which inserts raw HTML, is a decision. - Never trust input. Anything a browser sends can be changed, even the id in the URL or a hidden form field. Validate it on the server against a schema, a description of valid data.
- Secrets stay on the server. API keys and session secrets go in private runtime config: settings in
nuxt.config.tsthat only server code can read. Whatever reaches the browser is public. - Check who is asking, on the server. Hiding a button is not a check. Every API route asks who is signed in and whether this record or action is theirs.
- Patch your dependencies. Most of your code was written by strangers. Keep the lockfile, which pins exact versions, update often, and know what you ship.
Where the ten categories live in a Nuxt app
Each OWASP Top 10 risk tends to live in one part of a Nuxt app. OWASP, the Open Worldwide Application Security Project, is a non-profit that publishes free security standards. Its Top 10, rebuilt every few years from data on real applications, ranks risk categories; the 2025 edition is current. Each entry is a family of mistakes, not a single bug.
- BrowserA05
- Server routeA01 A07 A10
- Config and secretsA02 A04
- Packages and CIA03 A08
- LogsA09
- A01 Broken Access Control. Someone reaches data or actions that are not theirs, like Ana reading Ben's order, or a visitor choosing the URL your server fetches (server-side request forgery, SSRF).
- A02 Security Misconfiguration. Missing headers (browser rules in each response), a debug route left on, a key in
runtimeConfig.public, which every page receives. - A03 Software Supply Chain Failures. A package, build step or GitHub Action you trust is compromised. Look in
package.json, the lockfile and.github/workflows, your CI: the checks GitHub runs on each push. - A04 Cryptographic Failures. Passwords stored with a fast hash (a one-way scramble, quick to crack), data sent unencrypted over
http://. - A05 Injection. Input runs as code or as a query:
v-htmlwith a visitor's text, ajavascript:link, SQL database queries built from strings. - A06 Insecure Design. The plan is unsafe even when every line is correct, such as a password reset link that never expires.
- A07 Authentication Failures. Unlimited password guesses, sessions that never end, a login token kept in
localStorage, where any injected script can read it. - A08 Software or Data Integrity Failures. Data accepted without checking where it came from: a payment webhook (a provider's call to your server) without its signature.
- A09 Security Logging and Alerting Failures. An attack happens and nobody notices.
- A10 Mishandling of Exceptional Conditions. Errors that fail open or leak: internal error text in a response, a failed payment that still marks the order paid.
Two more documents, and when to use each
The Top 10 tells you what to look for. Two other OWASP documents tell you what to do. The Application Security Verification Standard (ASVS) 5.0 is a checklist of requirements in three levels. Level 1 holds the minimum; most applications should aim for Level 2. The Cheat Sheet Series has one how-to page per topic, such as Session Management.
Use the Top 10 when you design a feature: walk the ten categories and ask where it could go wrong. That is A06, Insecure Design, done cheaply. Use ASVS before a release, starting with Level 1, and a cheat sheet when you fix one thing.
What goes wrong
One route can fail three ways at once. Ana's order, with its receipt from a payment provider, comes from the Nuxt file server/api/orders/[id].get.ts, where [id] stands for any order number. The shop wrote requireUser(), which checks that someone is signed in, and db, its database helper; Nuxt provides the rest. Here is the broken version:
export default defineEventHandler(async (event) => {
await requireUser(event) // signed in, but never asks whose order this is
const order = await db.orders.find(Number(getRouterParam(event, 'id')))
const { paymentsKey } = useRuntimeConfig().public
try {
const receipt = await $fetch(`https://pay.example.com/v1/receipts/${order.paymentId}`, {
headers: { authorization: `Bearer ${paymentsKey}` },
})
return { ...order, receipt }
}
catch (error) {
throw createError({ status: 500, message: String(error) })
}
})
Ana requests order 1043 and gets Ben's: A01.
The route reads the payments key from useRuntimeConfig().public, the config Nuxt copies into every page's HTML. View any page's source and the key is there, inside window.__NUXT__.config: A02.
When the payment provider is down, String(error) hands the visitor the internal error text word for word. Production hides the message of an error you did not expect, but not one you pass to createError() yourself: A10.
The fixed route checks the owner, reads the key from private config and logs the details:
export default defineEventHandler(async (event) => {
const user = await requireUser(event)
const order = await db.orders.find(Number(getRouterParam(event, 'id')))
if (!order || order.ownerId !== user.id) {
throw createError({ status: 404, statusText: 'Order not found' })
}
const { paymentsKey } = useRuntimeConfig()
try {
const receipt = await $fetch(`https://pay.example.com/v1/receipts/${order.paymentId}`, {
headers: { authorization: `Bearer ${paymentsKey}` },
})
return { ...order, receipt }
}
catch (error) {
console.error('receipt lookup failed', order.id, error)
throw createError({ status: 502, statusText: 'Receipt unavailable' })
}
})
With the fix, Ana gets 404, Order not found, and a provider outage answers 502, Receipt unavailable. In the Code tab, nuxt.config.ts keeps the key out of public, so page sources show only the shop's name.
What to remember
- A security bug rarely breaks the page. It works for the wrong person, so ask who else can do this.
- Five habits prevent most of them: escape by default, never trust input, keep secrets and checks on the server, patch often.
- The Top 10:2025 is a map of risk families. Most live on the server, in config and in CI, not in components.
- Use the Top 10 when you design, ASVS Level 1 before you release, and the Cheat Sheets when you fix.
- Anything in
runtimeConfig.publicis in every page's HTML. Keys go in private runtime config. - Errors you expect get a short
statusText; the details go to your logs, never into the response.
In your project
Do these in a real project. Each one says how to confirm it worked.- In every file in
server/api, add a first-line comment naming its main category, such as// A01: reads an id from the URL.grep -rL 'A[01][0-9]' server/apiprints nothing once every file is tagged. Each A01 file should compare an owner before it returns anything. - Run
curl -sI https://your-site.example | grep -i -E 'content-security-policy|strict-transport-security|x-frame-options|referrer-policy'. Each missing line is a header you do not send yet; write them down for the Security misconfiguration lesson. - Open ASVS 5.0, chapter V8 Authorization, and find its four Level 1 requirements: 8.1.1, 8.2.1, 8.2.2 and 8.3.1. Next to each, write the file that meets it, such as the owner check in
orders/[id].get.tsfor 8.2.2. A requirement with no file is your next task.