Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
matthaywardwebdesign committed Dec 29, 2018
0 parents commit 3102c00
Show file tree
Hide file tree
Showing 11 changed files with 850 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .babelrc
@@ -0,0 +1,4 @@
{
"presets": ["es2015"],
"plugins": ["transform-object-rest-spread", "transform-class-properties"]
}
2 changes: 2 additions & 0 deletions .gitignore
@@ -0,0 +1,2 @@
node_modules
dist
154 changes: 154 additions & 0 deletions README.md
@@ -0,0 +1,154 @@
# NodeSecurity
:key: The easiest way to control what npm modules can access

## Overview
This repo / package was inspired a Medium post by David Gilbertson - [https://hackernoon.com/npm-package-permissions-an-idea-441a02902d9b](https://hackernoon.com/npm-package-permissions-an-idea-441a02902d9b)

> Imagine a package, created and maintained by npm (or someone equally trustworthy and farsighted). Let’s call it @npm/permissions.
> You would include this @npm/permissions package as the first import in your app, either in a file, or you run your app like node -r @npm/permissions index.js.
> This would override require() to enforce the permissions stated in a package’s package.json permissions property.
With the exception of some small differences, like not using package.json to manage permissions, this package
attempts to accomplish this goal.

## How it works
NodeSecurity works by overriding the Node.JS `require()` function, allowing us to enforce access constraints.

## Usage

```bash
npm install node-security
```

Firstly include NodeSecurity in your project at the very top of your applications entrypoint (before any other requires) and create a new instance.

```javascript
const nodesecurity = require( 'node-security' );
const NodeSecurity = new nodesecurity();
```

**Note:** If you're using the ES6 imports you'll need to create a seperate file that is imported at the entrypoint
of your application. Without doing this it won't be possible to configure NodeSecurity before any other modules are loaded.

**Configure NodeSecurity**

```javascript
NodeSecurity.configure({
/**
* The 'core' section controls
* global access to built in modules. By default
* all core modules are disabled.
*/
core: {
fs: true,
path: true,
/* You can disable specific module functions */
os: {
arch: false,
cpus: false,
}
},
/**
* The 'module' section controls
* per module access to built in modules. This allows
* us to disable access globally by allow it on a per
* module basis.
*/
module: {
axios: {
http: true,
https: true,
}
},
/**
* The 'env' section controls what environment
* variables are accessible via process.env
*/
env: {
API_KEY: true,
API_HOST: true,
}
});
```

:tada: **And you're done!** :tada:

All required / imported modules from this point onwards will have to be allowed by our configuration.

## Example

Here's an example script!

```javascript
/* Import and create a new instance of NodeSecurity */
const nodesecurity = require( 'node-security' );
const NodeSecurity = new nodesecurity();

/* Configure NodeSecurity */
NodeSecurity.configure({
core: {
/* Define global fs access */
fs: false,
/* Enable other core modules we'll need */
stream: true,
util: true,
path: true,
os: {
/* Deny access to OS arch */
arch: false,
},
assert: true,
},
module: {
/* Allow fs-extra to access fs */
'fs-extra': {
fs: true,
}
}
});

/* This won't throw an error as fs-extra is allowed to access fs */
require( 'fs-extra' );

/* Accessing fs directly will throw an error */
require( 'fs' );

/* Accessing os.arch will throw an error */
const os = require( 'os' );
os.arch();
```

## Plugins

You can extend the functionality of NodeSecurity by creating a plugin. For example you could create a plugin to allow http/s requests to only be made to specific servers.

An example plugin can be found at `src/plugins/NodeSecurityPlugin.js`

Plugins work by providing a way to override the default functionality of a core module. By default every Node core module (fs, os, etc) has a plugin loaded that allows for module methods to be disabled.

Including your own plugin is as simple as adding a plugins section to your configuration.

```javascript
plugins: {
http: MyHTTPPlugin
}
```

## Contributing

Building the package

```
npm run build
```

Running the test suite

```bash
npm test
```

## Ideas
- Include a set of default plugins that allow for more granular filesystem and network access.
36 changes: 36 additions & 0 deletions example/index.js
@@ -0,0 +1,36 @@
/* Import and create a new instance of NodeSecurity */
const nodesecurity = require( '../dist/index.js' );
const NodeSecurity = new nodesecurity();

/* Configure NodeSecurity */
NodeSecurity.configure({
core: {
/* Define global fs access */
fs: false,
/* Enable other core modules we'll need */
stream: true,
util: true,
path: true,
os: {
/* Deny access to OS arch */
arch: false,
},
assert: true,
},
module: {
/* Allow fs-extra to access fs */
'fs-extra': {
fs: true,
}
}
});

/* This won't throw an error as fs-extra is allowed to access fs */
require( 'fs-extra' );

/* Accessing fs directly will throw an error */
require( 'fs' );

/* Accessing os.arch will throw an error */
const os = require( 'os' );
os.arch();
24 changes: 24 additions & 0 deletions package.json
@@ -0,0 +1,24 @@
{
"name": "node-security",
"version": "0.0.1",
"description": ":key: The easiest way to control what npm modules can access",
"main": "dist/index.js",
"scripts": {
"build": "rimraf dist/ && babel ./src --out-dir dist/ --ignore **/*.test.js,./node_modules,./.babelrc,./package.json,./npm-debug.log --copy-files",
"test": "mocha --compilers js:babel-core/register --recursive src/"
},
"author": "Matt Hayward",
"license": "MIT",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"chai": "^4.2.0",
"fs-extra": "^7.0.1",
"mocha": "^5.2.0",
"rimraf": "^2.6.2",
"trash": "^4.3.0"
}
}
7 changes: 7 additions & 0 deletions src/Errors.js
@@ -0,0 +1,7 @@
const ERROR_ALREADY_CONFIGURED = 'NodeSecurity has already been configured.';
const ERROR_NOT_ALLOWED_TO_LOAD = ( module, parent ) => `NodeSecurity has blocked an attempt to access module '${module}'. Parent modules = ['${parent.join( ', ' )}']`;

export default {
ERROR_ALREADY_CONFIGURED,
ERROR_NOT_ALLOWED_TO_LOAD,
};
82 changes: 82 additions & 0 deletions src/ModuleLoader.js
@@ -0,0 +1,82 @@
import Module from 'module';
import Errors from './Errors';

class ModuleLoader {
constructor( config ) {
this.config = config;

/**
* Store an instance of the original Module._load
* functionaliy.
**/
this.originalLoad = Module._load;
}

getOriginalLoader = () => {
return this.originalLoad;
}

/* Determines whether this module was loaded by a parent module */
findParentModules( parent ) {
let parents = [];
let currentModule = parent;

/* Recursively walk up the parent tree, creating a list of parents */
while ( currentModule ) {
parents.push( currentModule.filename );
currentModule = currentModule.parent;
}

return parents;
}

isModuleAllowed( request, parent ) {
/* Determine the friendly module name of the parent module */
const parentModules = this.findParentModules( parent );

/**
* Loop over the parent modules, checking whether any of them specify
* individual module permissions. Keep in mind that individual module
* permissions will always override any core / global preferences.
* */
for ( let i = 0; i < parentModules.length; i++ ) {
const parentModule = parentModules[i];

/* Check whether config was provided for this module */
if ( this.config.module[ parentModule ] != null ) {
/* Config was provided, merge the config with the core config */
const moduleConfig = this.config.module[parentModule];

if ( moduleConfig[ request ] != null ) {
return moduleConfig[ request ];
}
}
}

/* Check whether we've blocked access to this module */
if ( this.config.core[ request ] != null && this.config.core[ request ] === false ) {
return false;
}

return true;
}

/* Called when a module is attempted to be loaded */
load = ( request, parent, isMain ) => {
/* Check whether loading this module is allowed */
const allowedToLoad = this.isModuleAllowed( request, parent );

/* If not allowed to load then throw an error */
if ( !allowedToLoad ) {
/* Get parent tree */
const parents = this.findParentModules( parent );
throw new Error( Errors.ERROR_NOT_ALLOWED_TO_LOAD( request, parents ));
}

const module = this.originalLoad( request, parent, isMain );

return module;
}
};

export default ModuleLoader;

0 comments on commit 3102c00

Please sign in to comment.