Securing Sensitive Craft CMS Assets with AWS Lambda

by Chris Chapman

Securing Sensitive Craft CMS Assets with AWS Lambda

During the early days of COVID-19, as more and more people were forced to work from home, one of our clients needed to quickly make their in-house Intranet externally available. Their Intranet is an essential resource for their employees and includes potentially sensitive files. One of the main challenges we needed to consider was how we could protect files and documents uploaded to the Intranet from being publicly available.

After evaluating our options, we decided that developing the Intranet into their existing Craft CMS based marketing site would be a quick, yet efficient approach. This would give their content editors an easy way to maintain Intranet content, while providing an authenticated way for users to access it.

We ultimately built the Intranet to take advantage of Craft CMS’ user system and an AWS S3 asset bucket delivered through CloudFront. This meant we already had a way to determine privileged access, and a hefty toolset from Amazon Web Services.

When an asset is uploaded to a typical Craft CMS volume, anyone with the URL can access it. The request never hits the application layer (i.e. Craft CMS or PHP) and there is not a way to reject requests at this level. One option could be generating tokenized URLs to facilitate the downloads, but this only provides obscurity without additional setup. Our ideal setup was having the public asset URLs allow or deny access based off if the user is currently authenticated with Craft CMS.

This is where AWS’ Lambda@Edge service comes into play. This is a feature of CloudFront which allows you to run serverless functions against requests. In other words, we are able to modify the response of HTTP requests depending on the request conditions.

At the Craft CMS level, we wrote a straightforward module to set a cookie whenever a user logs in. The cookie value contains a generated secret token stored in the application. This cookie is removed whenever the user logs out.

                                Event::on(
  User::class,
  User::EVENT_AFTER_LOGIN,
  function () {
    $this->setPrivateAssetCookie();
  }
);

Event::on(
  User::class,
  User::EVENT_AFTER_LOGOUT,
  function () {
    $this->removePrivateAssetCookie();
  }
);
                            

After that, we set up a Lambda function to handle the authorization. This checks if the requestor has a cookie matching a valid secret token mentioned above. If they do not, the response is modified to be a 403 forbidden error. Otherwise the request is left alone. This serves as a gatekeeper, and is configured to work with the CloudFront distribution and private S3 bucket. It runs on each and every request and results are not cached at edge locations. The serverless function also permits certain other requests, such as allowing the web server for Craft CMS asset transformations.

                                import {
  Callback,
  CloudFrontResponseEvent,
  CloudFrontResponseHandler,
  CloudFrontResultResponse,
  Context,
} from "aws-lambda";

// Check if the cookies contain a valid token for the assets.
export const isRequestAuthenticated = (
  cookies: { key?: string; value: string }[]
): boolean => {
  // ...
  return false;
};

// Check if the request is coming from a web server instance's IP address.
export const isBypassedIp = (ip: string): boolean => {
  // ...
  return false;
};

export const handler: CloudFrontResponseHandler = (
  event: CloudFrontResponseEvent,
  context: Context,
  callback: Callback<CloudFrontResultResponse>
) => {
  const { response, request } = event.Records[0].cf;

  if (
	!isRequestAuthenticated(request.headers.cookie) &&
	!isBypassedIp(request.clientIp)
  ) {
	response.body = "Forbidden";
	response.headers["content-type"] = [{ value: "text/html;charset=UTF-8" }];
	response.status = "403";
  }

  callback(null, response);
};
                            

Cloudfront events trigger lamba functions

We edit the behavior of the CloudFront distribution to associate the Lambda function at the origin response event.

Lambda function associaton

This is also where we choose to eliminate any caching at the distribution level. We then make sure that cookies are forwarded on to the Lambda function, which allows us to check the authorization token in our code above.

Cloudflare configuration

While the project may have been born out of necessity as a temporary and quick solution in an urgent situation, our client was so happy with the result that they made it a permanent replacement to their old Citrix/Wordpress Intranet site. Building the site in Craft not only means they have have one easy place to manage content, but we also have nearly unlimited capabilities for customizing the look and feel of it, so that it seamlessly carries over the branding and features of their marketing website.

Sometimes, necessity truly is the mother of invention!

More from the Blog:

Documentation for Remote Teams, using Notion

Documentation for Remote Teams, using Notion

Documentation is a crucial part of successful project communication – and even more so when you have a remote team. We know that it’s impossible to deliver a complex project successfully without documenting it from inception through post-launch.

Read More

A Developer’s Guide to Planning Documentation

A Developer’s Guide to Planning Documentation

If you’re a developer, chances are you reference documentation, but don't write your own. This article will help you write your own planning documentation.

Read More

Finally, stress-free web development.

Ready to partner with a team as interested in your success as you are? Reach out today for a no-pressure consultation.

Get Started

© Copyright Clearfire, Inc. Springfield, Illinois | Privacy Policy | hello@clearfirestudios.com | (217) 953-0321