Vibe coding has become a popular trend in the tech world. With so many tools now available for both developers and non-developers, it’s easier than ever to build projects using natural language, in some cases without touching a line of code along the way.

The problem is, speed often comes at the expense of security. There have been numerous reports of vibe coders facing attacks on their applications, with little understanding of the root causes or how to resolve them.

With this in mind we thought we’d put together five essential security practices to make your applications a little more secure. It’s certainly not an exhaustive list, but it’s a quick reference you can use to avoid the most obvious vibe-killing pitfalls.

1. Don’t Trust User Input

Even if you’re not writing raw SQL, your tools often are. Whether you’re using SQL databases directly, through ORMs or via calls to third-party APIs, SQL often plays a role along the way.

If that input is inserted into the query without proper safeguards, it can be manipulated by an attacker. One common example is SQL injection, where malicious input alters the structure of the query to give unauthorised access to your database.

Here’s an example of something you’re likely to do,
Unsafe example (manual query):

// 🚨 Vulnerable to SQL injection
const result = await db.query(
  `SELECT * FROM users WHERE email = '${req.body.email}'`
);

If a user sends a malicious email like ' OR '1'='1, the resulting SQL becomes:

SELECT * FROM users WHERE email = '' OR '1'='1'

This would return all users, completely bypassing any intended filters (as 1 is 1, so it’s always true).

Instead, we could use a parameterised query, like so:

// ✅ Parameterized query
const result = await db.query(
  'SELECT * FROM users WHERE email = $1',
  [req.body.email]
);

In our second example, the database engine sees the structure of the query first (SELECT * FROM users WHERE email = $1), receives the actual user input (req.body.email) as a separate value and treats that value only as data, never as part of the SQL code.

As an additional side-note, some platforms support Row-Level Security (RLS), which lets you define rules at the database level to control which rows a user can access. Enabling RLS gives you an added layer of protection, so we would also recommend using them where possible.

2. Secure Your Secrets

Despite being a well-known pitfall among developers, you would be surprised how often people push secrets like API keys or database credentials to GitHub, pass them to LLMs or even hard-code them in their code.

If exposed, these secrets can be misused and cost you money, with the attacker able max out quotas and rate limits and run up a huge bill in the process.

This can be avoided so easily. First of all, add an .env file to store your secrets in (otherwise known as environment variables).

Secondly, add your .env to the your .gitignore file. As the file-name suggests, this file tells GitHub which files it should ignore, so you don’t accidentally upload your .env file to GitHub.

Finally, make sure you keep your secrets secure. Whether you use a secret manager or tools like git-secrets, store the API key in an offline location, or even write them down in a notepad, just make sure you keep them safe.

3. Always Sanitise Inputs

Attackers can exploit inputs like form fields, URLs, or query parameters to inject scripts (XSS), break your application, or manipulate behavior.

Here’s an example of how this works. In this scenario, you allow users to add comments to a page, and you then render the comment without sanitising it first:

// User-submitted comment
res.send(`<div>${req.body.comment}</div>`);

Now imagine a bad actor submits the following text (code) as a comment, with it then being rendered:

<script>alert('hacked');</script>

This would then run in the browser of anybody who accessed the page, which could cause all kinds of issues.

To deal with this, validate and sanitise input on the server side, use schema validation libraries like Zod to make sure forms and inputs expect specific data types, and if rendering user-generated HTML, sanitise it using libraries like DOMPurify.

4. Add Rate Limits

APIs are surprisingly sensitive. Even with sanitised inputs, parameterised queries and secure secrets warding off the majority of attacks, unsophisticated techniques like abusing rate limits can put your application out of action.

Without basic limits in place, attackers or bots can overwhelm your endpoints by spamming forms, brute-forcing login attempts, or making excessive API calls.

To deal with this, you should apply rate limiting to all public-facing endpoints to limit the frequency that users can make calls to them.

For example, if you were using an Express server, you could use express-rate-limit like so:

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 20, // limit each IP to 20 requests per windowMs
});

app.use(limiter);

5. Don’t Roll Your Own Auth

Learning how to build your own authentication system might seem like a great way to learn more about the relationship between server and client, and how to handle tokens, cookies and sessions.

However, there’s a reason so many people stick to well-established auth libraries to handle authentication. Doing it manually can introduce serious vulnerabilities - from broken access control to session hijacking or token theft.

With this in mind, we recommend you use established authentication libraries or services like NextAuth.js, Supabase Auth, Clerk, or Auth0, with each one allowing you to confidently handle cookies, sessions and tokens, abstracting away a lot of the complexity that leads to errors and vulnerabilities.

Wrapping Up

When we think about vibe coding, security is a vibe-killer. With so much focus on building projects quickly, slowing down to add rate limits or refactor some non-parameterised queries can feel like a waste of time.

However, taking the time to apply these basics early will help you avoid unnecessary risks and technical debt later.

Security doesn’t have to be complex — it just needs to be part of your development process from the start so you can enjoy the vibes.

Other interesting articles:
JavaScript
See all articles