Simplify securing a public post endpoint (with recaptcha)

I have an angular app pulling data from strapi; pretty much a blog.
Strapi is excellent for providing anonymous access to data.
But another part of a website is collecting information from anonymous users. The canonical example would be comments, but in general - any organization might have their information that they’re interested in.
I could just expose a model and allow it to be created by public without any limitation, but that’s asking for trouble.
I could add a rate limit, but that doesn’t stop a million bots on different ips submitting a single spam each.

So I’m adding a custom recaptcha controller, which will take some JSON properties in the body:
The recaptcha code
The model to create
The data to put in the model

Which is great, but there’s a lot of manual work, checking the recaptcha, creating the data from the post ensuring that it matches the model schema, validating the model…

Ideally, I would mark a data type as “able to be created by anyone with recaptcha”, set my recaptcha key, and not have to do anything else server side.
And the client would post with an additional property called recaptcha_code, in addition to the model.

Alternatively, there could be a service to simplify the custom controller - something like query.create, where you pass in the recaptcha code, model name, and data (name/value or JSON, where names/keys must match schema), and it would return an error if recaptcha or schema validation fails.

Alternatively, there could be a new role called “Passed recaptcha”, and I can specify what additional things a person can do if they pass recaptcha. (similarly how now you can specify what a public user can do).

Really, it should be something somewhat generalized, so a person could use any sort of captcha or honeypot.

Thoughts?
Is this something that others have thought have? Find useful?
Should this be a plugin?

1 Like

Here’s something I put together - it’s been updated more in my repo, maybe I’ll make a plugin one day…

'use strict';
const recaptcha = require('recaptcha-validator');

const allowedPosts = ['exemption_request']

module.exports = {
  index: async (ctx, next) => {
    ctx.response.status = 400;

    const body = ctx.request.body;
    let code;
    let modelName;
    let modelData;

    if (body) {
      code = body.code;
      modelName = body.model;
      modelData = body.data;
    }

    if (!code) {
      ctx.send({error: 'No code'});
      return;
    }
    if (!modelName) {
      ctx.send({error: 'No model name'});
      return;
    }
    if (!modelData) {
      ctx.send({error: 'No data'});
      return;
    }

    if (!allowedPosts.includes(modelName)) {
      ctx.send({error: 'Forbidden'});
    }

    try {
      await recaptcha(process.env.RECAPTCHA_SECERET, code);
    } catch {
      ctx.send({error: 'Failed recaptcha'});
      return;
    }

    try {
      const newModel = await strapi.query(modelName).create(modelData);

      if (!newModel) {
        ctx.send({error: 'Model creation failed'})
      } else {
        ctx.response.status = 201;
        ctx.send(newModel);
      }
    } catch(err) {
      ctx.send({error: 'Model creation failed'})
    }
  }
};

2 Likes