Building a full-stack application is a rewarding challenge, but it's crucial to prioritize security from the very start. Have you considered the essential security principles needed to protect your product? How can we ensure that the application meets security standards effectively, and how easy is it to implement these principles?
In this article, I will explore fundamental security principles for both frontend and backend development. We’ll also discuss practical steps to implement them and ensure that your app adheres to industry security standards.
-Ensure that all input from users is properly validated and sanitized to avoid injection attacks such as Cross-Site Scripting (XSS).
-Use libraries like DOMPurify for sanitizing user inputs that will be inserted into the DOM.
-Implement secure authentication methods such as OAuth or the famous JWT method (JSON Web Tokens) to ensure that users are who they claim to be.
-Use role-based access control (RBAC) to restrict parts of the UI based on user permissions, for example part of the UI reserved to admin and other part for ordinary users.
// the code below show an example of how user //should be authenticated and depending //on his role the dashboard UI display const createRoutes = (logged: boolean, role:string) => createBrowserRouter([ { path: "/dashboard", loader: combinedDashboardLoader, element: ({role==="admin"}? ), errorElement:: , }])
-Avoid storing sensitive information like access tokens in localStorage or sessionStorage as they are vulnerable to XSS. Instead, use secure cookies with the HttpOnly and Secure flags enabled.
// in your server code you can write this code // inside your login controller function // so you don't need to send your token to the frontend res.cookie("accessToken", accessToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: expirationTime * 1000, sameSite: "lax", });
-Implement proper CORS policies to only allow specific origins to interact with your backend API. Ensure that you restrict methods and headers that are allowed.
-Set up a robust CSP to prevent the execution of untrusted scripts on your frontend. This reduces the risk of XSS attacks by controlling where resources can be loaded from.
-_Implementation _:
the easiest way is to simply use meta tags in your head of the html page as shown below in the code , for more details you can refer to this link https://www.stackhawk.com/blog/react-content-security-policy-guide-what-it-is-and-how-to-enable-it/
...
-Alternatively you can a middleware in your EpressJS as below:
app.use((req, res, next) => { res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' https://trusted.cdn.com; object-src 'none';"); next(); });
Enforce HTTPS to encrypt communication between the client and the server, ensuring that data is not exposed during transit.
-Always validate and sanitize user inputs on the server side to prevent SQL injection or other code injection attacks. Even if input validation is done on the frontend, it should always be performed on the backend as well.
below an example using express validator middleware, then simply apply this middleware before your registration controller function.
import { body, validationResult } from "express-validator"; const validateUserInput = [ body("email").isEmail().withMessage("Please enter a valid email address"), body("name") .isLength({ min: 3 }) .withMessage("Name must be at least 3 characters long"), body("password") .isLength({ min: 6 }) .withMessage("Password must be longer than 5 characters"), // Middleware to check for validation errors (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array()[0].msg }); } next(); }, ]; export default validateUserInput; //in your router file apply it //register is the post request function to add new user router.route("/register").post(validateUserInput, register);
-Use robust authentication mechanisms, such as OAuth, and secure tokens like JWT for session management.
-Implement role-based access control (RBAC) and avoid hardcoding permissions directly in your application.
-Store passwords securely using strong hashing algorithms like bcrypt or Argon2. Never store plain text passwords.
-If needed implement multi-factor authentication (MFA) for an additional layer of security.
the code below shows how the process of securing password and using JWT (authentication).
// hashing password inside your registration function const register = async (req: Request, res: Response) => { const { email, password, name } = req.body; const hashedPassword = await bcrypt.hash(password, 10); createUser(email,name,hashedpassword) //a function to add new user into your DB } // Compare the password to the hashed one during the login request const login = async (req: Request, res: Response) => { const { email, password } = req.body; const existingUser = await query(findUserByEmail, [email]); if (!existingUser[0]) return res.status(404).json({ message: `${email} not found! register if you don't have an account`, }); const matchPassword = await bcrypt.compare( password, existingUser[0].password ); //generate token JWT const expirationTime = 180; const accessToken = jwt.sign( { id: existingUser[0].iduser }, process.env.ACCESS_TOKEN_SECRET!, { expiresIn: expirationTime "s", } );}
Rate Limiting:
It restricts the number of requests a user (identified by IP, API key, etc.) can make to the server in a given time window (e.g., 100 requests per minute). After the limit is reached, further requests are blocked or delayed.
const express = require('express'); const rateLimit = require('express-rate-limit'); const app = express(); // Define rate limiting rule: 100 requests per 15 minutes const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests message: 'Too many requests from this IP, please try again later.', // Custom message standardHeaders: true, `RateLimit-*` headers legacyHeaders: false, }); // Apply the rate limiting middleware to all requests app.use(limiter); //but we can also apply it to a specific route app.post('/login', limiter, (req, res) => { res.send('Login route'); }); app.listen(3000, () => { console.log('Server is running on port 3000'); });
Throttling:
Similar to rate limiting, but instead of blocking requests entirely, it slows down the processing of requests after a certain threshold is reached. This can avoid service overloads.
Example:
If a user makes more than 10 API requests per second, you slow down the response to every subsequent request to ensure the system isn’t overwhelmed.
Use encryption for sensitive data both at rest and in transit. Ensure that databases and other storage mechanisms encrypt sensitive information.
the code below shows an implementation of data encryption of a credit card number before storing it in the database.
const crypto = require('crypto'); // Encryption settings const encryptionKey = crypto.randomBytes(32); // AES-256 key const iv = crypto.randomBytes(16); // Initialization vector // Encrypt function function encrypt(text) { const cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted = cipher.final('hex'); return `${iv.toString('hex')}:${encrypted}`; // Store both IV and encrypted text } // Post request app.post('/user', (req, res) => { const { creditCard, email } = req.body; // Encrypt the name const encryptedCard = encrypt(creditCard); // Save encrypted to the database const user = new User({ creditCard: encryptedCard , email: email });// example of sequelize queries user.save().then(() => { res.send('User saved successfully'); }).catch(err => { res.status(500).send('Error saving user'); }); });
When you need to display the real data (in this case the card number) , you can use decrypt function inside your GET request.
// Decrypt function function decrypt(encryptedData) { const [ivHex, encryptedText] = encryptedData.split(':'); const ivBuffer = Buffer.from(ivHex, 'hex'); const decipher = crypto.createDecipheriv('aes-256-cbc', encryptionKey, ivBuffer); let decrypted = decipher.update(encryptedText, 'hex', 'utf8'); decrypted = decipher.final('utf8'); return decrypted; } //GET request app.get('/user/:id', (req, res) => { const userId = req.params.id; User.findById(userId).then(user => { if (!user) return res.status(404).send('User not found'); // Decrypt the user's card const decryptedCard = decrypt(user.creditCard); res.json({ creditCard: decryptedCard , email: user.email }); }).catch(err => { res.status(500).send('Error retrieving user'); }); });
-Apply HTTP security headers like Strict-Transport-Security, X-Frame-Options, X-XSS-Protection, and X-Content-Type-Options to protect against various common vulnerabilities.
The simplest way to implement it is by using the helmet middleware in express after installing it npm install helmet.
const express = require('express'); const helmet = require('helmet'); const app = express(); app.use(helmet()); app.listen(3000, () => { console.log('Server is running on port 3000'); });
The alternative implementation is to manually setting Headers without Helmet as shown as below.
app.use((req, res, next) => { res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' https://trusted.cdn.com"); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); res.setHeader('X-XSS-Protection', '1; mode=block'); res.setHeader('Referrer-Policy', 'no-referrer'); res.setHeader('Permissions-Policy', 'geolocation=(self), microphone=()'); next(); });
-Implement Cross-Site Request Forgery (CSRF) protection mechanisms such as CSRF tokens to prevent unauthorized actions in your app.
How CSRF Attacks Work:
-The user logs into your app, and the server sets a session cookie or an authentication token.
-The user then visits a malicious site while still logged in to your app in the background.
-The malicious site sends a request to your app using the logged-in user's credentials (session cookie) to perform unintended actions like transferring money or changing account details.
To prevent CSRF attacks, you need to ensure that requests made from your frontend are authenticated and originate from a trusted source. The most common way to protect against CSRF is by using CSRF tokens.
Implementation is easy by using cookie parser and express.urlencoded middlewares
first install them npm install csurf cookie-parser
const express = require('express'); const cookieParser = require('cookie-parser'); const csrf = require('csurf'); const app = express(); // Use cookie-parser for CSRF token storage (if using cookie-based tokens) app.use(cookieParser()); // Initialize the CSRF middleware const csrfProtection = csrf({ cookie: true }); // Use body-parser to parse form data app.use(express.urlencoded({ extended: false })); // routes here after applying the middlewares app.listen(3000, () => { console.log('Server is running on port 3000'); });
-Ensure proper logging of security-related events such as failed login attempts and potential XSS attacks. Also, set up monitoring and alerts for abnormal behavior.
This article aimed to highlight and summarize the key security principles that should be considered when developing full-stack JavaScript applications. We also explored practical ways to implement these principles efficiently. Additionally, these insights can be valuable when preparing for job interviews, especially when discussing various security measures and best practices in app development.
免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。
Copyright© 2022 湘ICP备2022001581号-3