10xEngineer

Security Holes You Can't Ignore

Security Holes You Can't Ignore

"Hey, check this out - I can edit anyone's profile on your app."

That's not a message you want to get from your users. But last week, a startup founder got exactly this message. Their response? "But we check for valid access tokens!"

Yeah... about that. Let me show you some security holes that might be lurking in your code right now. Grab your coffee - this is going to be fun.

Security Holes That Could Sink Your Startup

1. The "Anyone Can Be Anyone" Bug

Here's the scariest five lines of code I keep seeing:

// DANGEROUS: Your users can probably do this right now
app.put("/api/users/:userId/profile", validateToken, async (req, res) => {
  const { userId } = req.params;
  await db.users.update(userId, req.body);
  res.json({ success: true });
});

Want to be an admin? Just change the userId in the URL. Game over. Here's how to actually do it:

// SAFE: Actually check who's who
app.put("/api/users/:userId/profile", validateToken, async (req, res) => {
  const { userId } = req.params;
  const tokenUserId = req.user.id;
 
  if (userId !== tokenUserId) {
    return res.status(403).json({ error: "Nice try! 😉" });
  }
 
  const updates = sanitizeUserInput(req.body); // Always sanitize!
  await db.users.update(userId, updates);
  res.json({ success: true });
});

2. The "Infinite Password Guesses" Feature

Who doesn't love a good brute force playground?

// DANGEROUS: Attackers love endpoints like these
app.post("/api/login", async (req, res) => {
  const { email, password } = req.body;
  const user = await db.users.findOne({ email });
 
  if (!user || !(await comparePassword(password, user.password))) {
    return res.status(401).json({ error: "Invalid credentials" });
  }
 
  const token = generateToken(user);
  res.json({ token });
});

5,000 password attempts per second? No problem! Unless you do this:

// SAFE: Rate limiting is your friend
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // Limit each IP to 5 requests per window
  message: {
    error: 'Too many attempts. Maybe try "password123"? Just kidding.',
  },
});
 
app.post("/api/login", loginLimiter, async (req, res) => {
  const { email, password } = req.body;
 
  const user = await db.users.findOne({ email });
  if (!user || !(await comparePassword(password, user.password))) {
    await logFailedAttempt(req.ip, email);
    return res.status(401).json({ error: "Invalid credentials" });
  }
 
  const token = generateToken(user);
  res.json({ token });
});

3. The "Drop All Tables" Button

SQL injection in 2025? You bet!

// DANGEROUS: One semicolon away from disaster
app.get("/api/search", async (req, res) => {
  const { query } = req.query;
  const result = await db.query(
    `SELECT * FROM products WHERE name LIKE '%${query}%'`
  );
  res.json(result);
});

Try searching for '; DROP TABLE products; -- and watch the world burn. Or do this instead:

// SAFE: Parameterized queries save lives
app.get("/api/search", async (req, res) => {
  const { query } = req.query;
  const result = await db.query("SELECT * FROM products WHERE name LIKE $1", [
    `%${query}%`,
  ]);
  res.json(result);
});

4. The "Trust All User Input" Philosophy

Ever seen this in your React code?

// DANGEROUS: XSS paradise
function UserProfile({ user }) {
  return (
    <div>
      <h1>Welcome!</h1>
      <div dangerouslySetInnerHTML={{ __html: user.bio }} />
    </div>
  );
}

Congratulations, you just let users inject any JavaScript they want! Here's the fix:

// SAFE: Sanitize that input!
import DOMPurify from "dompurify";
 
function UserProfile({ user }) {
  return (
    <div>
      <h1>Welcome!</h1>
      <div className="bio">{user.bio}</div>
      {/* If you MUST allow HTML: */}
      <div
        dangerouslySetInnerHTML={{
          __html: DOMPurify.sanitize(user.bio, {
            ALLOWED_TAGS: ["b", "i", "em", "strong"],
          }),
        }}
      />
    </div>
  );
}

5. The "localStorage is My Database" Anti-Pattern

// DANGEROUS: Storing sensitive data in localStorage
class AuthService {
  login(token, userDetails) {
    localStorage.setItem("token", token);
    localStorage.setItem("userDetails", JSON.stringify(userDetails));
  }
}

Any JavaScript on your domain can read that. Try this instead:

// SAFE: Use httpOnly cookies for sensitive data
class AuthService {
  async login(credentials) {
    const response = await fetch("/api/login", {
      method: "POST",
      credentials: "include", // This is important!
      body: JSON.stringify(credentials),
    });
 
    // Server sets httpOnly cookie
    const { user } = await response.json();
    return user;
  }
}
 
// Server-side
app.post("/api/login", (req, res) => {
  // ... validate credentials ...
  res.cookie("token", token, {
    httpOnly: true,
    secure: true,
    sameSite: "strict",
  });
  res.json({ user: safeUserData });
});

6. The "Client-Side Only" Validation Trap

// DANGEROUS: Client-side validation only
function TransferMoney({ amount }) {
  const handleTransfer = () => {
    if (amount <= userBalance) {
      api.transfer(amount);
    } else {
      showError("Insufficient funds");
    }
  };
}

Anyone can open DevTools and bypass your checks. Always validate server-side:

// SAFE: Trust no one
// Client-side
function TransferMoney({ amount }) {
  const handleTransfer = async () => {
    // Client validation for UX
    if (amount <= userBalance) {
      try {
        await api.transfer(amount);
      } catch (error) {
        // Handle server validation errors
        showError(error.message);
      }
    }
  };
}
 
// Server-side
app.post("/api/transfer", async (req, res) => {
  const { amount } = req.body;
  const user = await getUser(req.user.id);
 
  if (amount > user.balance) {
    return res.status(400).json({ error: "Nice try! 🎣" });
  }
 
  // Proceed with transfer
});

The "You Need This Now" Security Checklist

  1. Check your user profile endpoints - can users modify other profiles?
  2. Search for dangerouslySetInnerHTML in your React code
  3. Look for direct SQL queries without parameters
  4. Check what you're storing in localStorage
  5. Add rate limiting to your auth endpoints
  6. Enable CORS properly (no * in production!)
  7. Use security headers (helmet.js is your friend)
  8. Log sensitive operations (but not sensitive data!)

What's Next?

Perfect security doesn't exist, but "good enough" security does. Start with the examples above - they'll protect you from the most common attacks that actually happen in the real world.

Got questions about your security? Found a hole you're not sure how to fix? Let's chat. Security is like a puzzle, and I love solving puzzles.

P.S. - Now's a good time to check your profile update endpoint. I'll wait. 😉

Let's Turn Your Idea Into Reality