Security Measures on Posts CRUD Feature

CRUD feature for posts had been implemented in this site since November 8th, 2023. Almost all contents of this page, such as primary banner, about page, blog posts, and news, are actually posts on the site’s database. They are then fetched from database and displayed as HTML on their appropriate locations.

To create new posts, we simply have to access /posts page and we would be presented with full CRUD feature on managing posts. One can create, retrieve, update, and delete posts from the interface. One can even see drafts of posts.1

It would not be sane to have the site showcased as is with its exposed administrative interface. So far the site survives only because it is relatively obscure, and no one really have the knowledge to access it. However security through obscurity is highly discouraged, and by extension this site’s only defense must not be secured through obscurity.

As auth is yet to be implemented, we would have to protect ourselves from possible vandalism via other means. The solutions we have at our disposal are as follows:

  1. Through IP whitelisting, while denying access from everywhere else.
  2. Through auth_basic directive in nginx.
  3. Implement auth on Laravel.

The third solution is obvious, but we lack the time to do it as of now. Therefore we will explore the first two options.

IP Whitelisting

In nginx, we can configure it to deny access from all other IP addresses but from our own IP address:

# file /etc/nginx/sites-available/oursite.conf
...
    location /posts {
        allow 1.2.3.4;
        deny all;
        proxy_pass http://127.0.0.1:8088/posts;
        include proxy_params;
    }
...

Then if we access /posts from any IP addresses but the one we whitelisted, it would result in a 403: Forbidden HTTP error. However the drawback is that we have to whitelist all of our allowed IP addresses. It would also mean that if one of the allowed IP addresses is compromised, it could be used to vandalize our site.

Nginx auth_basic

We can configure nginx to limit access to specific resources using “HTTP Basic Authentication” protocol.

# file /etc/nginx/sites-available/oursite.conf
...
    location /posts {
        auth_basic "Restricted Access";
        auth_basic_user_file conf/htpasswd;
        proxy_pass http://127.0.0.1:8088/posts;
        include proxy_params;
    }
...

Then we can add allowed credentials to file /etc/nginx/conf/htpasswd in the following format:

# comment
name1:password1
name2:password2:comment
name3:password3

The file can contain variables.

We can generate the password using openssl passwd on Linux CLI.

$ openssl passwd
Password: 
Verifying - Password: 
$1$WlepJCJ9$B0MS2tZNt7nhsdu78G4221

Note that for security, the typed password would not be visible on the terminal. The resulting hash (from our example above is $1$WlepJCJ9$B0MS2tZNt7nhsdu78G4221) can then be put in our htpasswd file:

# file /etc/nginx/conf/htpasswd
admin:$1$WlepJCJ9$B0MS2tZNt7nhsdu78G4221

Then when we open the /posts route, we would be presented with a pop up that requests our username and password. Once we fill it up and submit the form, we would be able to access the resource.

If we fail to submit the form, or if we submit a wrong credential, we would be presented with a 401: Authorization Required HTTP error.

Custom Error Pages

Laravel provides us with a way to show HTTP error pages. We display its use in our Controller.php, especially on fetchPost($slug) and fetchPostById($id) methods:

# File app/Http/Controllers/Controller.php
    public function fetchPost($slug) {
        $post = Post::select(
                    'posts.created_at as created',
                    'post_types.code as type',
                    'posts.slug',
                    'posts.title',
                    'users.name as author',
                    'posts.body'
                    )
                    ->where('is_published',1)
                    ->where('posts.slug',$slug)
                    ->join('users','users.id','=','posts.users_id')
                    ->join('post_types','post_types.id','=','posts.post_types_id')
                    ->first();
        if (!$post) abort(404);
        return $post;
    }

    public function fetchPostById($id) {
        $post = Post::select(
                    'posts.id',
                    'posts.created_at as created',
                    'post_types.code as type',
                    'posts.slug',
                    'posts.title',
                    'users.name as author',
                    'users.id as author_id',
                    'posts.body',
                    'posts.is_published'
                    )
                    ->where('posts.id',$id)
                    ->join('users','users.id','=','posts.users_id')
                    ->join('post_types','post_types.id','=','posts.post_types_id')
                    ->first();
        if (!$post) abort(404);
        return $post;
    }

The line if (!$post) abort(404); basically tells the function to throw 404 HTTP error when $post is null as the requested post is not available in our database.

To replace default Laravel error pages, we added three files under resources/views/errors/:

$ tree -CR resources/views/errors/
resources/views/errors/
├── 401.blade.php
├── 403.blade.php
└── 404.blade.php

Those files would correspond to error 401 (authorization required), 403 (forbidden), and 404 (page not found).

Then we can add in our web.php the following lines:

# File routes/web.php

// Error pages
Route::get('/401', function () { abort(401); });
Route::get('/403', function () { abort(403); });
Route::get('/404', function () { abort(404); });

The purpose of adding those routes is to allow nginx to redirect corresponding HTTP error status to our custom pages. Therefore our nginx server block should appear to be as follows:

server {
    listen [::]:443 ssl; 
    listen 443 ssl; 
    server_name liecorp.id;
    include certs_params_liecorp.id;
    error_page 401 /401;
    error_page 403 /403;
    error_page 404 /404;
    location / {
        proxy_pass http://127.0.0.1:8088;
        include proxy_params;
    }
    location /posts {
        auth_basic "Restricted Access";
        auth_basic_user_file conf/htpasswd;
        proxy_pass http://127.0.0.1:8088/posts;
        include proxy_params;
    }
}

  1. Drafts are by default, not visible on the public facing side.↩︎