Search code examples
javascriptnode.jsroutesdirectorycpanel

NodeJS: User can bypass route handler by navigating directory


I'm new to node.js and I've created a login system. My app is in the subdirectory of a domain, for example: https://domain.tld/nrjs/. There are two folders within this app which contain static files. One folder is called public and one is called protected. When the user connects to the node app at https://domain.tld/nrjs/, they are served the static folder public and directed to the login.html page. Once they are authenticated they are then served the static folder protected, and sent to https://domain.tld/nrjs/index.html which is stored in the protected folder. The issue is, it seems whether the app.js sends them the folder or not (whether they have authentication or not), the user can directly link to the folder at https://domain.tld/nrjs/protected/index.html or any other actual file, and it completely ignores any route handlers in my app.js file. I'm using cPanel. Below is my app.js, but it seems like the user can directly link to the file and app.js isn't even involved. I need to be able to prevent the user from accessing the protected folder until they have successfully logged in, or preventing them from accessing it at all is fine too, as I will be sending them the files at /index.html not /protected/index.html.

Structure:

public_html/
│
└───nrjs/                 (node.js app is in subdirectory) 
    │   app.js
    │
    ├───public/
    │       login.html    (Publicly accessible login page)
    │
    └───protected/
            index.html    (Private home page, should not be directly accessible)

Here is my app.js:

const express = require('express');
const fs = require('fs');
const path = require('path');
const session = require('express-session');
const bodyParser = require('body-parser');
const bcrypt = require('bcryptjs');

const app = express();
const basePath = '/nrjs';
const saltRounds = 10;

let users = [
    { username: 'admin', password: 'password' },
    { username: 'user1', password: 'password1' },
    { username: 'user2', password: 'password2' }
];

Promise.all(users.map(user =>
    bcrypt.hash(user.password, saltRounds).then(hash => {
        user.password = hash;
    })
)).then(() => {
    console.log('Users initialized with hashed passwords');
});

app.use(session({
    secret: 'KAL783BB4HJK1P90V3',
    resave: false,
    saveUninitialized: true
}));

app.use(bodyParser.urlencoded({ extended: true }));

app.use(basePath, express.static(path.join(__dirname, 'public')));

function checkAuthenticated(req, res, next) {
    if (!req.session.user && req.path !== '/login.html' && req.path !== '/login') {
        return res.redirect(basePath + '/login.html');
    }
    next(); 
}

app.use(basePath, checkAuthenticated, express.static(path.join(__dirname, 'protected')));

app.post(basePath + '/login', (req, res) => {
    const { username, password } = req.body;
    const user = users.find(u => u.username === username);

    if (user) {
        bcrypt.compare(password, user.password, (err, isMatch) => {
            if (err) {
                console.error('Error during password comparison:', err);
                return res.redirect(basePath + '/login.html?error=An error occurred');
            }

            if (isMatch) {
                req.session.user = user.username;

                const loginLog = `Successful login by ${user.username} at ${new Date().toLocaleString()}\n`;
                fs.appendFile('logs/success_login_attempts.log', loginLog, (err) => {
                    if (err) {
                        console.error('Error writing to login log file:', err);
                    }
                });

                res.redirect(basePath + '/index.html');
            } else {
                const failedLoginLog = `Failed login attempt with username: ${username} at ${new Date().toLocaleString()}\n`;
                fs.appendFile('logs/failed_login_attempts.log', failedLoginLog, (err) => {
                    if (err) {
                        console.error('Error writing to failed login log file:', err);
                    }
                });

                res.redirect(basePath + '/login.html?error=Incorrect username or password');
            }
        });
    } else {
        const failedLoginLog = `Failed login attempt with username: ${username} at ${new Date().toLocaleString()}\n`;
        fs.appendFile('logs/failed_login_attempts.log', failedLoginLog, (err) => {
            if (err) {
                console.error('Error writing to failed login log file:', err);
            }
        });

        res.redirect(basePath + '/login.html?error=Incorrect username or password');
    }
});

app.get(basePath + '/login', (req, res) => {
    res.redirect(basePath + '/login.html');
});

app.get(basePath + '/logout', (req, res) => {
    req.session.destroy();
    res.redirect(basePath + '/login.html');
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

I've tried putting all kinds of route handlers at the top of the page, and did a catch-all route handler and it still let the user directly navigate to the file.

Example:

app.get('/nrjs/protected', (req, res) => {
    res.status(403).send('Access Denied');
});

I also tried using a route wildcard:

app.get('/nrjs/protected/*', (req, res) => {
    res.status(403).send('Access Denied');
});

and when trying to catch everything they can still navigate to the protected file:

app.use((req, res, next) => {
    res.status(404).send("Sorry, the page you're looking for doesn't exist.");
});

I have also tried with blocking access to the protected folder using .htaccess and got a 503 error on routes outside of the protected folder.

Edit: I've moved the node app outside of the web directory, and therefore the user's can no longer directly navigate to the apps static files, fixing the issue. However, I'd still like to know if there is a way to prevent the user from navigating the files if I did store the node app within the web directory. I can confirm that there is no session set when the user successfully navigates to the protected directory. Thanks!


Solution

  • All of the files that can be accessed via HTTP such as .jpg, .css, .js, .txt or .html that are in your public or protected directory can be accessed in the browser simply by navigating to them in the url. This because whatever is serving files from the public_html/ directory (Apache likely) is the one serving up protected/index.html and not express. Here is an illustration.

    This is your domain:

    https://domain.tld
    

    User makes a GET request to this file:

    https://domain.tld/pancakes.html
    

    Apache (for arguments sake) serves up the file from here:

    /var/www/html/public_html/pancakes.html
    

    User then makes a GET request to this route for example:

    https://domain.tld/nrjs/login
    

    Well you must have something listening for traffic on port 443 to https://domain.tld/nrjs and routing it to port 3000 so that express can pick it up.

    When express gets that request on port 3000 it then sends a redirect to nrjs/login.html. Well you are using the app.use(basePath, express.static(path.join(__dirname, 'public'))); middleware so express sees login.html in the public folder and just sends the html back to the user.

    Next the user sends a POST request to here:

    https://domain.tld/nrjs/login
    

    Again its re-routed to port 3000 and after a few password validations etc express redirects to nrjs/index.html. It passes through the static middleware looking at the public directory and can't find it. Then it passes through the static middleware looking at the protected directory and bingo, it serves up index.html.

    Now what you have is a checkAuthenticated middleware making sure it's an authenticated user looking for index.html but that will only work for requests made on port 3000 to https://domain.tld/nrjs/index.html.

    If the user navigates to:

    https://domain.tld/nrjs/protected/index.html
    

    Then that it looks like Apache is just picking that up on port 443 and serving it up. Express doesn't have any route handlers for protected/index.html, it only has nrjs/index.html.

    My advice would be:

    1. Get rid of the protected folder all together.
    2. Move index.html to the same directory as app.js.
    3. Change this:
    res.redirect(basePath + '/index.html');
    

    to this

    res.sendFile(__dirname + '/index.html')
    
    1. Delete this:
    app.use(basePath, checkAuthenticated, express.static(path.join(__dirname, 'protected')));
    
    1. Add this:
    app.get(basePath + '/index', checkAuthenticated, (req, res) => {
        res.sendFile(__dirname + '/index.html')
    });
    

    Now the only way a user can access index.html is by going through the login process or by going through the https://domain.tld/nrjs/index route which they will need to pass the checkAuthenticated middleware first.