Discover essential security best practices for vibe coding, including password hashing, rate limiting, input validation, JWT protection, HTTPS, CSRF prevention, and MFA.
Vibe coding has changed how developers build software. With AI-powered coding assistants, you can create websites, dashboards, APIs, and full-stack applications by describing what you want in natural language.
However, building an application quickly does not automatically make it secure.
AI-generated code can contain vulnerabilities, incorrect authentication logic, weak validation, exposed secrets, or insecure database queries. An application may work perfectly during development while remaining vulnerable to attacks in production.
Whether you are building with Next.js, React, Node.js, Express, Strapi, or another framework, understanding basic security principles is essential.
In this guide, we will explore the most important security practices for vibe coding and explain how to protect your applications from common attacks.
1. Protect Against Brute-Force Attacks
A brute-force attack occurs when an attacker repeatedly attempts to guess a user's password or authentication credentials.
For example, an attacker might send hundreds or thousands of login requests using different password combinations until one succeeds.
How to prevent brute-force attacks
Implement rate limiting: Restrict how many login attempts a user or IP address can make within a specific period.
Use progressive delays: Increase the waiting time after repeated failed attempts.
Implement account protection: Temporarily restrict suspicious login activity without allowing attackers to permanently lock out legitimate users.
Add multi-factor authentication: Require an additional verification step for sensitive accounts.
Monitor failed login attempts: Log suspicious patterns and alert administrators when necessary.
For Node.js applications, you can use libraries such as express-rate-limit to limit repeated requests.
Remember that IP-based limits alone are not enough. Attackers can distribute requests across multiple IP addresses, so combine rate limiting with account-level controls and monitoring.
2. Store Passwords Securely with bcrypt or Argon2
Never store passwords as plain text in your database.
If an attacker gains access to your database, plain-text passwords immediately expose user accounts. Even simple encryption is not an adequate replacement for a dedicated password-hashing algorithm.
Use a password-hashing algorithm designed to resist offline password cracking, such as Argon2id or bcrypt.
Example using bcrypt
import bcrypt from "bcrypt";
const password = "user-password";
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(
password,
saltRounds
);
// Verify a password during login
const isValid = await bcrypt.compare(
password,
hashedPassword
);
The example demonstrates the basic process. In production, select work factors appropriate for your server capacity and current security guidance.
Important password security rules
Never save the original password.
Never log passwords or include them in error messages.
Use a unique salt for every password; bcrypt handles this automatically.
Consider Argon2id for new applications when supported.
Require strong passwords without relying only on arbitrary complexity rules.
Protect password-reset tokens and make them expire after a short period.
If you are using a framework or authentication provider that already handles password hashing, use its established implementation rather than creating your own.
3. Implement Rate Limiting Across Your APIs
Rate limiting is not only for login endpoints. Public APIs, search features, registration forms, password resets, and expensive operations can also be abused.
An attacker may repeatedly call an endpoint to consume CPU, database connections, memory, or third-party API credits.
Example with Express
import rateLimit from "express-rate-limit";
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 100,
standardHeaders: "draft-7",
legacyHeaders: false,
});
app.use("/api/", apiLimiter);
This example limits requests to 100 per 15-minute window for each client identifier recognized by the middleware.
The appropriate limit depends on your application. Login and password-reset endpoints should generally have stricter limits than ordinary read-only endpoints.
Best practices
Apply stricter limits to authentication and account-recovery routes.
Set appropriate limits for expensive database queries and file uploads.
Use a shared rate-limit store, such as Redis, when running multiple application instances.
Configure trusted proxies correctly so attackers cannot bypass IP-based limits by spoofing forwarding headers.
Return HTTP 429 Too Many Requests when a client exceeds its limit.
Rate limiting reduces abuse, but it does not replace authentication, authorization, or infrastructure-level DDoS protection.
4. Prevent Cross-Site Scripting (XSS)
Cross-Site Scripting (XSS) occurs when an application allows untrusted content to execute as JavaScript in another user's browser.
For example, if a comment or profile description is rendered as executable HTML, an attacker may inject a script that steals accessible session information or performs actions as the victim.
How to prevent XSS
Avoid inserting untrusted content directly into HTML.
Use framework features that escape text by default.
Sanitize user-generated HTML if your application genuinely needs to display it.
Avoid unsafe rendering features such as React's dangerouslySetInnerHTML unless there is a well-controlled reason to use them.
Use a Content Security Policy (CSP) as an additional defense.
Validate URLs before allowing them in links or other sensitive attributes.
React example
Render user content as ordinary text:
function UserComment({ comment }) {
return <p>{comment}</p>;
}
React escapes text content automatically in this context.
Avoid treating arbitrary user input as trusted HTML. If rich-text rendering is required, use a reputable sanitizer and a carefully configured policy.
Important: Input validation alone does not eliminate XSS. Context-aware output encoding, safe rendering, and appropriate sanitization are also necessary.
5. Prevent Denial-of-Service (DoS) Attacks
A Denial-of-Service attack attempts to make an application unavailable by overwhelming its resources.
Attackers may send excessive requests, upload huge files, trigger expensive operations, or exploit endpoints that consume too much memory or processing time.
Practical protection strategies
Apply rate limiting to public endpoints.
Limit request body sizes and uploaded file sizes.
Set timeouts for requests and external service calls.
Add pagination and maximum page-size limits to database queries.
Avoid unbounded loops, uncontrolled concurrency, and expensive operations triggered by every request.
Cache frequently requested data when appropriate.
Use a reverse proxy, web application firewall, or DDoS protection service where needed.
Monitor CPU, memory, database connections, response times, and traffic patterns.
For example, an endpoint that returns every record in a large database may become a performance and availability problem even without a malicious attacker. Pagination and query limits help prevent this.
For large-scale distributed denial-of-service attacks, application-level rate limiting is not sufficient by itself. Infrastructure and network-level protections may be necessary.
6. Prevent NoSQL Injection
NoSQL injection can occur when an application passes untrusted input into a database query without ensuring that the input has the expected structure and meaning.
In MongoDB-backed applications, a vulnerable login query might accept an object where a string password is expected. Depending on how the query is constructed, this may allow an attacker to manipulate the query logic.
Example of unsafe code
const user = await users.findOne({
email: req.body.email,
password: req.body.password,
});
This is problematic if the application assumes the request body contains only strings and uses the values directly in database operations.
Safer approach
Validate the input types, use a password-hashing system, and keep database queries under application control.
const { email, password } = req.body;
if (
typeof email !== "string" ||
typeof password !== "string"
) {
return res.status(400).json({
error: "Invalid input",
});
}
const user = await users.findOne({ email });
if (!user) {
return res.status(401).json({
error: "Invalid credentials",
});
}
const isValid = await bcrypt.compare(
password,
user.passwordHash
);
if (!isValid) {
return res.status(401).json({
error: "Invalid credentials",
});
}
This example assumes users is a database collection and passwordHash contains a properly generated password hash.
Additional defenses
Validate request bodies using a schema-validation library.
Reject unexpected fields and unexpected data types.
Never allow clients to submit arbitrary database operators.
Avoid constructing queries directly from unrestricted request objects.
Restrict database permissions to the minimum required.
Keep database drivers and dependencies updated.
These principles apply to other database systems as well: user input should never be allowed to control query behavior unexpectedly.
7. Secure JWT Authentication
JSON Web Tokens (JWTs) are commonly used to authenticate users in web applications and APIs. However, incorrect token handling can introduce serious security problems.
Common JWT vulnerabilities
Accepting tokens without verifying their signatures.
Allowing unexpected signing algorithms.
Using weak or exposed signing secrets.
Setting excessively long token lifetimes.
Failing to validate token expiration or intended audience.
Storing long-lived tokens in locations accessible to malicious JavaScript.
Failing to implement a sensible revocation or session-invalidation strategy.
JWT security best practices
Use a well-maintained authentication library.
Verify the token signature and explicitly restrict accepted algorithms.
Validate relevant claims, including expiration, issuer, and audience when applicable.
Use strong, randomly generated secrets or securely managed signing keys.
Keep access tokens short-lived.
Use refresh-token rotation and reuse detection where appropriate.
Plan how to revoke sessions after logout, account compromise, or password changes.
Never put passwords, private keys, or other secrets inside JWT payloads.
A JWT is generally signed, not encrypted. Its payload can usually be decoded by anyone who obtains the token, so do not place sensitive information in it.
Where should tokens be stored?
There is no single storage choice that is ideal for every application.
HttpOnly cookies: Help prevent JavaScript from reading the cookie, but require careful CSRF protection and appropriate Secure and SameSite settings.
Browser storage: Local storage is accessible to JavaScript, which means successful XSS attacks may expose tokens stored there.
Choose a token-storage strategy based on your application's architecture and threat model. Avoid implementing authentication from scratch when a trusted framework or provider can handle it securely.
8. Enforce HTTPS Everywhere
HTTPS encrypts traffic between clients and servers, helping protect credentials, session cookies, and other sensitive information from interception or modification in transit.
Without HTTPS, an attacker on an untrusted network may be able to observe or manipulate insecure communications.
HTTPS checklist
Use a valid TLS certificate.
Redirect HTTP requests to HTTPS.
Configure secure TLS settings.
Set session cookies with the Secure attribute.
Enable HTTP Strict Transport Security (HSTS) after verifying that your HTTPS configuration is correct.
Avoid mixed content, where an HTTPS page loads resources over HTTP.
Ensure API endpoints, authentication callbacks, and administrative interfaces use HTTPS.
For production deployments, use your hosting platform or reverse proxy to manage certificates and redirects where appropriate.
HTTPS does not protect against every attack. It does not fix insecure authorization, SQL or NoSQL injection, or vulnerable application code.
9. Prevent Cross-Site Request Forgery (CSRF)
Cross-Site Request Forgery occurs when an attacker tricks a user's browser into sending an unwanted request to an application where the user is already authenticated.
This is particularly relevant to applications that authenticate requests using cookies, because browsers may automatically attach those cookies.
How to prevent CSRF
Use appropriate SameSite cookie settings.
Implement CSRF tokens for state-changing requests when required by your authentication architecture.
Validate the Origin header where appropriate.
Consider Fetch Metadata headers as an additional defense.
Never rely on GET requests for operations that change important application state.
Use explicit authorization checks on every sensitive endpoint.
For cookie-based sessions, SameSite=Lax or SameSite=Strict can reduce CSRF risk, depending on the application's requirements. They should be selected carefully rather than treated as a universal replacement for every other defense.
Important: Bearer tokens sent explicitly in an Authorization header are generally not automatically attached by browsers in the same way as cookies. CSRF risk therefore depends on how authentication credentials are transported and how the application is configured.
10. Add Multi-Factor Authentication (MFA)
Passwords alone may not be enough to protect sensitive accounts. Users can reuse passwords, fall for phishing attacks, or have credentials exposed in data breaches.
Multi-factor authentication adds another verification step.
Common options include:
Authenticator apps: Generate time-based one-time passwords.
Passkeys: Use public-key cryptography and can provide strong phishing resistance.
Hardware security keys: Provide strong protection against many credential-theft attacks.
SMS codes: Offer an additional barrier but are generally less secure than phishing-resistant methods.
Where should MFA be used?
Prioritize MFA for:
Administrator accounts.
Users with access to sensitive customer information.
Financial and billing operations.
Account recovery and security-setting changes.
Production infrastructure and deployment systems.
For the strongest protection, consider passkeys or hardware security keys where supported.
MFA should be paired with secure account recovery, session management, and monitoring. A weak recovery process can undermine an otherwise strong MFA implementation.
11. Validate Input and Enforce Authorization
Many vulnerabilities happen because developers trust data received from the browser.
A frontend may hide administrative buttons or prevent users from entering invalid values, but attackers can bypass these controls by sending requests directly to the API.
Validate all untrusted input
Use schema-validation tools such as Zod, Joi, or your framework's built-in validation features to verify:
Required fields.
Data types and formats.
String lengths.
Numeric ranges.
Allowed enum values.
Nested object structures.
Maximum array lengths and request sizes.
For example, an API that accepts a page number should reject negative values and excessively large page sizes.
Enforce authorization on the server
Authentication answers the question: "Who is this user?"
Authorization answers: "What is this user allowed to do?"
Every sensitive operation must enforce authorization on the server. Never rely only on frontend route guards, hidden buttons, or user-submitted role values.
For example, a user should not be able to access another customer's invoice merely by changing an ID in the URL.
Follow the principle of least privilege: give users, application services, and database accounts only the permissions they need.
12. Protect API Keys and Environment Variables
AI-generated applications sometimes accidentally expose credentials in frontend code, public repositories, logs, or deployment configuration.
Exposed credentials can allow attackers to access databases, consume paid APIs, or take control of infrastructure.
Secret-management best practices
Store sensitive values in environment variables or a dedicated secret manager.
Never commit .env files containing real credentials to Git.
Keep server-only secrets out of browser bundles.
In Next.js, remember that environment variables prefixed with NEXT_PUBLIC_ are intended to be exposed to the browser.
Restrict access to production secrets.
Rotate credentials if they are exposed.
Use separate credentials for development, testing, and production.
Avoid placing API keys in client-side prompts, public source code, or error messages.
If a secret is accidentally published, deleting the file or Git commit is not enough. Assume the credential may have been copied and rotate it immediately.
13. Secure File Uploads
File uploads are another common attack surface, especially in applications with profile pictures, document management, or content-management systems.
An attacker may upload oversized files, malicious content, or files designed to exploit vulnerable parsers.
File-upload security checklist
Limit file sizes.
Allow only necessary file types.
Validate file content rather than trusting the extension or client-provided MIME type.
Generate server-side filenames instead of trusting uploaded filenames.
Store uploads outside executable application directories when possible.
Prevent uploaded files from being executed as server-side code.
Require authorization for private file uploads and downloads.
Scan files for malware when the risk and environment justify it.
For public applications, consider storing uploads in object storage and serving them from a separate origin. This can reduce the impact of certain file-serving vulnerabilities.
14. Keep Dependencies and AI-Generated Code Under Review
Vibe coding makes it easy to add packages and generate large amounts of code quickly. However, every dependency and generated function can introduce security risks.
AI-generated code should be treated as code that requires review, testing, and maintenance.
A practical security workflow
Review authentication, authorization, database queries, and file-upload logic.
Run dependency vulnerability checks.
Remove unused packages and unnecessary permissions.
Keep frameworks and dependencies updated.
Use static analysis and security linting where available.
Add automated tests for access-control rules and validation failures.
Review changes before deploying to production.
Never paste production secrets into AI prompts unless the environment and data-handling policies explicitly permit it.
Tools such as GitHub Dependabot, npm audit, and supported static-analysis tools can help identify known dependency problems. Their results should be reviewed rather than blindly accepted.
15. Test Security Before Deployment
An application that works correctly is not necessarily secure. Security testing should be part of the development process, even for small projects.
Essential pre-deployment checks
Test repeated login failures and rate-limit behavior.
Verify that users cannot access other users' private data.
Test API endpoints with missing, malformed, and unexpected input.
Check that expired or invalid tokens are rejected.
Confirm that sensitive configuration is not exposed to the browser.
Test file-size and file-type restrictions.
Review security headers and HTTPS configuration.
Check dependencies for known vulnerabilities.
Inspect logs for leaked credentials or sensitive personal data.
Test account recovery and logout behavior.
For a structured checklist, use the OWASP Application Security Verification Standard (ASVS) and relevant OWASP guidance as references for your application's risk level.
Only test systems you own or have explicit permission to assess.
Security Checklist for Vibe Coding Projects
Before launching an AI-built application, verify the following:
Passwords are hashed using Argon2id or bcrypt.
Login and sensitive endpoints have appropriate rate limits.
User input is validated on the server.
Database queries cannot be manipulated through untrusted input.
Authorization is checked for every sensitive operation.
XSS risks are addressed through safe rendering and sanitization where needed.
JWTs and sessions are configured securely.
HTTPS is enforced in production.
Cookie-based authentication has appropriate CSRF protections.
MFA or passkeys protect sensitive accounts where appropriate.
API keys and secrets are not exposed publicly.
File uploads have size, type, and access restrictions.
Dependencies are checked for known vulnerabilities.
Errors and logs do not reveal sensitive information.
Security testing is included in the deployment workflow.
Conclusion
Vibe coding can significantly accelerate application development, but speed should never replace security.
AI tools can generate useful code, suggest implementations, and help developers solve complex problems. However, they cannot guarantee that an application is secure simply because it runs successfully.
By implementing password hashing, rate limiting, input validation, secure authentication, HTTPS, CSRF protection, and proper authorization, you can reduce many common security risks.
The most important principle is simple: treat AI-generated code as a starting point, not a security guarantee.
Build security into your development workflow from the beginning, test your assumptions, review sensitive code, and verify your application's behavior before deploying it to production.

