Skip to content
Snippets Groups Projects
validate.js 4.22 KiB
Newer Older
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// a teensy tinsy module to do parameter validation.  A good candiate for future
// librification.
//
// usage:
//
//   const validate = require('validate.js');
//
//   app.post('/wsapi/foo', validate([ "email", "site" ]), function(req, resp) {
//   });

const
logger = require('./logging.js').logger,
httputils = require('./httputils.js'),
check = require('validator').check;

var types = {
  email: function(x) {
    check(x).isEmail();
  },
  password: function(x) {
    check(x).len(8,80);
  },
  boolean: function(x) {
    if (typeof x !== 'boolean') throw "boolean required";
  },
  token: function(x) {
    check(x).len(48,48).isAlphanumeric();
  },
  assertion: function(x) {
    check(x).len(50,10240).regex(/[0-9a-zA-Z~_-]+/);
  },
  pubkey: function(x) {
    check(x).len(50,10240);
    JSON.parse(x);
  },
  origin: function(x) {
    /* origin regex
    /^                          // beginning
    https?:\/\/                 // starts with http:// or https://
    (?=.{1,254}(?::|$))         // hostname must be within 1-254 characters
    (?:                         // match hostname part (<part>.<part>...)
      (?!-)                     // cannot start with a dash (allow it to start with a digit re issue #2042)
      (?![a-z0-9\-]{1,62}-      // part cannot end with a dash
        (?:\.|:|$))             // (end of part will be '.', ':', or end of str)
      [a-z0-9\-]{1,63}\b        // part will be 1-63 letters, numbers, or dashes
        (?!\.$)                 // final part cannot end with a '.'
        \.?                     // part followed by '.' unless final part
    )+                          // one or more hostname parts
    (:\d+)?                     // optional port
    $/i;                        // end; case-insensitive
    */
    var regex = /^https?:\/\/(?=.{1,254}(?::|$))(?:(?!-)(?![a-z0-9\-]{1,62}-(?:\.|:|$))[a-z0-9\-]{1,63}\b(?!\.$)\.?)+(:\d+)?$/i;
    if (typeof x !== 'string' || !x.match(regex)) {
      throw new Error("not a valid origin");
  // normalize the parameters description, verify all specified types are present
  if (Array.isArray(params) || typeof params !== 'object' || typeof params === null) {
    throw "argument to validate must be an object, not a " + (typeof params);
  }

  Object.keys(params).forEach(function(p) {
    var v = params[p];
    if (typeof v === 'string') {
      v = { type: v };
    }
    if (typeof v.required === "undefined") v.required = true;

    if (!types[v.type]) throw "unknown type specified in WSAPI:" + v.type;
    params[p] = v;
  });

    // clear body and query to prevent wsapi handlers from accessing
    // un-validated input parameters
    req.body = {};
    req.query = {};
    req.params = {};

    // now validate
      // allow csrf through
      if (reqParams.csrf) {
        req.params.csrf = reqParams.csrf;
        delete reqParams.csrf;
      }

      Object.keys(params).forEach(function(p) {
        if (params[p].required && !reqParams.hasOwnProperty(p)) throw "missing required parameter: '" + p + "'";
        if (reqParams[p] === undefined) return;

        // validate
        try {
          types[params[p].type](reqParams[p]);
        } catch (e) {
          throw p + ": " + e.toString();
        req.params[p] = reqParams[p];
        delete reqParams[p];

      // if there are any keys left in reqParams, they're not allowable!
      var extra = Object.keys(reqParams);
      if (extra.length) throw "extra parameters are not allowed: " + extra.join(', ');
      var msg = {
        success: false,
        reason: e.toString()
      };
      logger.warn("bad request received: " + msg.reason);
      resp.statusCode = 400;
      return resp.json(msg);
    // this is called outside the try/catch because errors
    // in the handling of the request should be caught separately
    next();