N1ED acts as Drupal 8 module. It uses API of CKEditor module to add a new plugin into CKEditor.
N1ED is a composite CKEditor plugin: it includes features of 5 CKEditor plugins well-integrated with each other to let them work in synergy.
Main features are: new widgets, Bootstrap layout editor, including blocks palette and custom blocks, preview content on mobile devices, file uploader, file manager, image editor, and many many more smaller features.

Project link

https://www.drupal.org/project/n1ed

GIT instructions

git clone --branch 8.x-2.x https://git.drupalcode.org/project/n1ed.git

PAReview checklist

https://pareview.sh/pareview/https-git.drupal.org-project-n1ed-8.x-2.x-0

Comments

dmitriy-komarov created an issue. See original summary.

Ankush_03’s picture

Status: Needs review » Needs work

Thanks for your contribution !

Please fix below warning
Review of the 8.x-2.x branch (commit 7fb92fe):
DrupalPractice has found some issues with your code, but could be false positives.

FILE: ...rupal/pareviewsh/pareview_temp/flmngr/lib/composer/autoload_real.php
--------------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
--------------------------------------------------------------------------
9 | WARNING | Class name must be prefixed with the project name
| | "N1ed.info"
--------------------------------------------------------------------------

Time: 4.48 secs; Memory: 8Mb

shaktik’s picture

Please also add in info file to support d9 core_version_requirement: ^8 || ^9
https://www.drupal.org/docs/8/creating-custom-modules/let-drupal-8-know-...

dmitriy-komarov’s picture

Status: Needs work » Needs review

Ankush,

As I wrote in the ticket, this is false positive alarm.
"N1ed.info" is not correct module name: ".info" is incorrectly parsed by PAReview in some cases, please check this related bug of PAReview.

shaktik,

Drupal 9 support was added yesterday, checked and published. Module did not use any deprecated API, so it was easy. Please check again.

apaderno’s picture

Issue summary: View changes
Status: Needs review » Needs work

The module includes code that is licensed under GPLv3: https://packagist.org/packages/edsdk/flmngr-server-php and https://packagist.org/packages/edsdk/file-uploader-server-php. Those libraries should be added as Composer dependencies. Including them in drupal.org repositories, you are releasing them as GPLv2+.

dmitriy-komarov’s picture

kiamlaluno

I'm the author of this code and it's OK to license it as GPLv2+.

These libraries were adopted for Drupal and included as built-in code due to original libraries did not fit Drupal coding standards. There is no conflict with Composer users use to update Drupal. I actually thought this is required to move them into the code and adopt to Drupal coding standard, am I wrong?

Is it fine if now I keep code as is?
In future I can move it out as Composer dependencies.

apaderno’s picture

Dependencies that are available on external sites should not be committed in the drupal.org repositories.

dmitriy-komarov’s picture

kiamlaluno,

I've made some changes in the code and removed all Composer stuff from N1ED repo.
But I decided to keep file manager server package into Drupal module and here are the reasons:

  1. Adding "the must" Composer dependencies will break working for all who already installed N1ED but does not have a Composer: update will destroy anything.
  2. This will make unable to install N1ED from the scratch for those who does not have Composer installed.
  3. Also we plan to port this module to Drupal 7 and having no Composer dependencies will be useful there.

The main problem is Drupal 8 does not guarantee that Composer is installed on the server. There are many users with shared hosting who will be unable to use Composer (or will have too much difficulties related to this). So I think it will be better to move this code inside of the module. I'm the author of both module and PHP package, so I will contribute all the code as a part of N1ED. Please confirm this is OK.

dmitriy-komarov’s picture

Status: Needs work » Needs review
apaderno’s picture

Assigned: Unassigned » apaderno

I will review the code in the next 12 hours.

apaderno’s picture

Issue tags: +PAreview: security
  • What follows is a quick review of the project; it doesn't mean to be complete
  • For every point, I didn't make a complete list of where the code should be fixed, but an example of what is wrong in the code
  • Not all the points are application stoppers; some of them describe changes that would be preferable to make
/**
 * {@inheritdoc}
 */
function n1ed_help($path, $arg) {
  switch ($path) {
    case "help.page.n1ed":
      return "

                <h3>About N1ED</h3>
            
                <p><a href=\"https://n1ed.com\">N1ED add-on for CKEditor</a> adds a lot of features to your editor. N1ED is a multi-add-on meaning it will connect different plugins from N1ED Ecosystem which you specify in the preferences.</p>


                <h3>Installation</h3>
                
                <p>The installation process is typical for Drupal 8 - just install N1ED module, all dependencies will be linked automatically. Also this module will attach to those Text Formats which have CKEditor and are fine to be used by article editors. For example it will attach to \"Full format\" and will not to \"Plain text\" or \"Basic HTML\".</p>
                
                
                <h3>Configuration</h3>
                
                <p>Acting as standard CKEditor Drupal 8 submodule N1ED will be enabled in those Text Formats which have CKEditor and where N1ED is not disabled. Go to the <a href=\"/admin/config/content/formats\">Text Formats page</a> and you can see the badge \"N1ED\" near formats N1ED is marked enabled in. Go into text format to configure N1ED there by clicking \"Configure\" button.</p>
                
                <p>When you are on some text format page, first you need to set you N1ED API key once. N1ED configuration widget will lead you through this simple process and attach existing N1ED account or register a new one for free. Your default API key is demo key, and it is also workable but does not have access to some online services, so we recommend you to change it first of all.</p>
                
                <p>Why do you need to link an account? It's easy: because your API key is a reference to your configuration. N1ED will be auto updated using CDN, also cloud of N1ED provides some services like getting screenshots of custom blocks you define, storing configurations and sharing them between your different websites if required, fast switching configurations, and more services in future versions.</p>
                
                <p>Then you can enable or disable N1ED for each text format independently. It is recommended to use N1ED in text formats available for administrator/articles editor users and disable for restricted formats which used in comments form or somewhere like it.</p>
                
                
                
                <h3> Editing an articles</h3>
                
                <p>You will go to the article page as before and edit your content with CKEditor powered with N1ED, Bootstrap Editor, File Manager, Image Editor and other currently available and available in future plugins for CKEditor which are published inside <a href=\"https://n1ed.com/plugins\">N1ED Ecosystem</a>.</p>
                
                
                
                <h3>Troubleshooting</h3>
                
                <p>If you do not see these features on your CKEditor, please be sure you chose appripriate Text Format (which has N1ED attached). In some cases your default text format can be \"Basic HTML\" which may require to switch fo \"Full HMTL\" in the Drupal combobox right under CKEditor area.</p>
                
                <p>In case of any problems please check <a href=\"https://n1ed.com/docs\">documentation</a> or <a href=\"mailto:support@n1ed.zendesk.com\">ask support via e-mail</a>.</p>
            
            ";
  }
}

{@inheritdoc} is not used for hook implementations, which are documented, for example, as Implements hook_help().

Any string shown to the users needs to be translatable. The full output of the hook_help() implementation and other functions isn't translatable.

flmngr:
  path: '/flmngr'
  defaults:
    _controller: '\Drupal\n1ed\Controller\FlmngrController::flmngr'
    _title: 'Flmngr file manager'
  requirements:
    _permission: 'edit own content'

edit own content isn't a permission Drupal core nor this module defines. It's also not clear in which way that page is related to content (i.e. nodes).

n1edSetApiKey:
  path: '/n1ed/setApiKey'
  defaults:
    _controller: '\Drupal\n1ed\Controller\N1EDController::setApiKey'
    _title: 'Set API key request'
  requirements:
    _permission: 'administer site configuration'

With a path like /n1ed/setApiKey, the settings page will not be visible from any page. Settings pages normally have path starting with /admin/config.

class N1EDController extends ControllerBase {

  /**
   * Drupal\Core\Config\ConfigFactoryInterface definition.
   *
   * @var Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * {@inheritdoc}
   */
  public function __construct(ConfigFactoryInterface $config_factory) {
    $this->configFactory = $config_factory;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory')
    );
  }

  /**
   * Sets API key into Drupal config.
   */
  public function setApiKey() {
    if (!isset($_POST["apiKey"])) {
      throw new AccessDeniedHttpException();
    }

    $config = $this->configFactory->getEditable('n1ed.settings');
    $config->set('apikey', $_POST["apiKey"]);
    $config->save(TRUE);

    return new Response();
  }

}

Settings are changed using Drupal forms. How are administrator users supposed to send a POST request to set the API key?
Drupal code should not directly access $_POST.

include $this->fileSystem->realpath(drupal_get_path('module', 'n1ed') . '/flmngr/flmngr.php');

Now that Drupal 8 support PSR4, code isn't included that way anymore. Eventually, if a PHP file needs to be included, there is the module_handler service.

apaderno’s picture

Status: Needs review » Needs work
dmitriy-komarov’s picture

Hi kiamlaluno,

Thank you for hints. I've added translations calls, created two new permissions and changed URL of internal request, also now N1ED module uses Drupal's PHP autoloader to load all its modules.

Please let me explain call to "setApiKey" URL. We really need it because our CMS-independent configuration widget requires this call (to save API key now waiting before user clicks "Save" and submits the form).
This is reasonable: this widget allows to register an account on N1ED and create API key linked to it. If user will not save the form after creating an account he will lose info about this account and API key. So widget makes Ajax call to internal URL ("setApiKey") and the key is being saved into DB.

dmitriy-komarov’s picture

Status: Needs work » Needs review
apaderno’s picture

Assigned: apaderno » Unassigned
Status: Needs review » Needs work
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('file_system'),
      $container->get('request_stack')
    );
  }

  /**
   * Calls file manager server side and returns a result to client.
   */
  public function flmngr() {

    FlmngrServer::flmngrRequest(
      [
        'dirFiles' => realpath(".") . '/sites/default/files/flmngr',
        'dirTmp' => realpath(".") . '/sites/default/files/flmngr-tmp',
        'dirCache' => realpath(".") . '/sites/default/files/flmngr-cache',
      ],
      $this->requestStack
    );

    return new Response();
  }

The file_system service isn't used.

  public function setApiKey() {
    if (!isset($_POST["apiKey"]) || !isset($_POST["token"])) {
      throw new AccessDeniedHttpException();
    }

    $config = $this->configFactory->getEditable('n1ed.settings');
    $config->set('apikey', $_POST["apiKey"]);
    $config->set('token', $_POST["token"]);
    $config->save(TRUE);

    return new Response();
  }

Since the controller is using the request_stack service, it should use that service to get the value of $_POST["token"] and $_POST["apiKey"]. Modules should never use $_POST.

/**
 * Defines plugin.
 *
 * @CKEditorPlugin(
 *   id = "N1EDEco",
 *   label = @Translation("N1ED"),
 *   module = "n1ed"
 * )
 */
abstract class N1EDEcosystemCKEditorPlugin extends CKEditorPluginBase implements CKEditorPluginConfigurableInterface, CKEditorPluginContextualInterface {

That class cannot be a plugin, since it uses the wrong namespace. Also, an abstract class cannot be a plugin.

  /**
   * Returns plugin name.
   *
   * @return string
   *   Plugin name is a name of add-on in CKEditor terms
   */
  abstract public function getPluginName();

  /**
   * Returns module name.
   *
   * @return string
   *   Module name is a name of module in Drupal terms
   */
  abstract public function getModuleName();

  /**
   * Returns buttons list.
   *
   * @return array
   *   Associative array like in @CKEditorPlugin->getButtons
   */
  abstract public function getButtonsDef();

  /**
   * Adds controls to form using this class methods.
   */
  abstract public function addControlsToForm(&$form, $editor, $config);

Is there a reason those functions aren't defined in a interface?

dmitriy-komarov’s picture

Abstract class was just a legacy from the times when there were a number of modules instead of one united N1ED module. Now this class was removed and the code was simplified.
All other issues were fixed too.

Code was pushed into GIT, but not published as a new module version due to there were no new features, just refactoring.
So I hope we will publish the next module version already with the green shield badge :)

dmitriy-komarov’s picture

Status: Needs work » Needs review
klausi’s picture

Status: Needs review » Needs work
  1. n1ed.routing.yml: you are using "administer site configuration" here, but shouldn't you use your own permission that you define in the permission file?
  2. /admin/config/n1ed/setApiKey is vulnerable to CSRF exploits. You need to confirm user intent either by a confirmation form or by using _csrf_token on the route. See https://www.drupal.org/docs/8/api/routing-system/access-checking-on-rout...
  3. Same for FlmngrController. It performs data changing actions via the FlmngrServer class and never verifies user intent to protect against CSRF.

This is currently a security blocker.

dmitriy-komarov’s picture

Status: Needs work » Needs review

Hi Klausi,

Thank you for security review.

1. Just a mistype. Of course we need to use our own permission, fixed.
2 and 3. CSRF checking is added into D8 routes declaration, also support of CSRF tokens was added to client parts of N1ED too.

klausi’s picture

Status: Needs review » Needs work

Thanks!

  1. routing file: all route names must be prefixed with your module name to avoid name clashes with other modules.
  2. If you use _csrf_request_header_token then you also need the route requirement to only allow POST requests. Looking a the logic of CsrfRequestHeaderAccessCheck I see that it does not apply to GET requests, so the vulnerability still works?
dmitriy-komarov’s picture

Status: Needs work » Needs review

1. Prefixed all route names with module name.
2. Removed all GET requests from both client and server code; added only POST request as the only permitted in routes definition.

Could you check again?

klausi’s picture

Status: Needs review » Needs work

Thanks!

  1. class FlmngrServer: doc block just repeats the class name. What is this class used for and why? Please update all your class comments to descirbe what they are doing.
  2. class N1ED: "Configure add-on" is user facing text and must run through $this->t() for translation.
  3. The error handling of invalid requests seems broken. If I send a POST request with Postman to https://drupal-8.ddev.site/flmngr?f=/etc/passwd.jpg then I get {"error":"Array","data":null}. Please check your error handling and make sure you return meaningful messages.
  4. In FMDiskFileSystem you hard-code "/" as directory separator. That looks like a security risk for Windows operating systems where "\" is used. Best use the PHP constant DIRECTORY_SEPARATOR instead.
  5. FMDiskFileSystem::getRelativePath(): why do you check the root dir name for slashes here and throw an exception if it does not contain slashes? Please add a code comment. I tried to exploit https://drupal-8.ddev.site/flmngr?f=/etc/passwd.jpg here but could not get any further (which is good, just wondering how this is supposed to work even in non-malicious cases).

I played around a little bit with the file manager service, but could not get an exploit to work with uploading PHP files or trying to read arbitrary files on the server. So that looks safe from my testing.

Now I'm just worried about if an attacker could read arbitrary files somehow, please clarify my questions above.

dmitriy-komarov’s picture

Status: Needs work » Needs review

Hi Klausi,

We needed a time to test the new changes related to DIRECTORY_SEPARATOR and now I'm back.

  1. Comments for all files were updated and extended.
  2. Fixed.
  3. There was just a bug with generating of error messages in some cases. This error value "Array" needs to be serialized into JSON structure with error code and description, now it's working fine.
  4. Every call to file system now is being processed DIRECTORY_SEPARATOR.
  5. Of course this "root" is not the root of file system. This is the root of directory where Flmngr stores uploaded files (sites/default/files/flmngr by default). But the first slash is just a requirement of our Flmngr client-server protocol.
klausi’s picture

Status: Needs review » Fixed

OK thanks, looks good to me.

Thanks for your contribution, Dmitriy!

I updated your account so you can opt into security advisory coverage now.

Here are some recommended readings to help with excellent maintainership:

You can find lots more contributors chatting on Slack or IRC in #drupal-contribute. So, come hang out and stay involved!

Thanks, also, for your patience with the review process. Anyone is welcome to participate in the review process. Please consider reviewing other projects that are pending review. I encourage you to learn more about that process and join the group of reviewers.

Thanks to the dedicated reviewer(s) as well.

dmitriy-komarov’s picture

Thanks to all for reviewing the module.
I very appreciate your work, and happy from being a part of Drupal community.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.