I'm trying to build a sort of database-based ACL for my Express application. I currently have a Permissions table in the database that contains something like that:
Resource: *
Method: *
RoleID: 1
isAllowed: true
Resource: /users
Method: GET
RoleID: 2
isAllowed: false
Resource: /users/id/*
Method: GET
RoleID: 2
isAllowed: true
My aim is to build a middleware that checks on the request object and allows or denies the route based on the rules in the database. My practical problem is, how do I match, say /users/id/1 with the database entry /users/id/*? If I use the database entry as the basis for my regex, then /users/id/1 is clearly matched, however, I don't think it's practical to pull and test ALL database entries for each request. What do you think would be the best approach to obtain the right rule from the database based on the requested URL?
Thanks for your time!
OK, after a bit of thinking and research, I've discovered that you can use regular expressions in MySQL queries, so I've come up with this middleware (I'm using Sequelize):
module.exports = function (req, res, next) {
// If a wildcard is in place, skip the rest
return models.Permissions.findAll({
where: {
resource: '*',
GID: req.session.role,
isAllowed: 1
}
}).then(function (result) {
if (result[0]) {
return next()
}
// If the URL contains more than one element, replace the last item with [item, *]
// to match eventual wildcards in the database entries
let urlItems = req.url.split('/').filter(Boolean)
let url = req.url
if (urlItems.length > 1) {
let lastItem = '[' + urlItems[urlItems.length - 1] + ', *]'
url = req.url.split('/')
url[url.length - 1] = lastItem
url = url.join('/')
}
let query = 'SELECT * FROM Permissions '
query += 'WHERE resource RLIKE "^' + url + '?$" '
query += 'AND GID = ' + req.session.role
return models.sequelize.query(query, {
type: models.sequelize.QueryTypes.SELECT
}).then(function (result) {
let policy = result[0]
function return403 () {
res.status(403).send('Forbidden')
}
// Forbid everything by default
if (!policy) {
return403()
return
}
let methods = policy.method.toUpperCase().split(' ')
// Forbid all methods which are not allowed
if (policy.method === '*' || methods.includes(req.method)) {
if (!policy.isAllowed) {
return403()
return
}
}
// When other methods are explicitly allowed, forbid everything else
if (policy.method !== '*' && !methods.includes(req.method) && policy.isAllowed) {
return403()
return
}
// Standard behaviour: allow explicitly allowed methods (or *) that are allowed.
next()
})
})
}
Related
I have this code installed on my website and what it is suppose to do is see if a product has a single or double attribute and then it disables products from there that are out of stock so customers can't click on it. The code works great for the single attribute products but it doesn't seem to work for the double attribute products.
Single Attribute Product link - https://true-grit-running-company.shoplightspeed.com/goodr-sunglasses.html
Double Attribute Product link - https://true-grit-running-company.shoplightspeed.com/womens-bondi-7.html?id=65554420&quantity=1
`
// A quick check to see if it is a product being viewed (checking the microdata) - to avoid running the rest of the code if viewing a page other than the product page
if ($('[itemtype*="//schema.org/Product"]').length > 0) {
//Check the url to see if a variant is being viewed or not
var curl = location.href;
//choose the appropriate ajax url
if (curl.indexOf('?') > -1) {
var url = curl + '&format=json';
} else {
var url = '?format=json';
}
//Start the ajax call
$.ajax({
url: url,
})
// Add the disabled attribute to the variants that aren't available
.done(function(obj) {
//create a variable with the product variants
var data = obj.product.variants;
//fun a function on each variant
$.each(data, function(index, value) {
//check if any of the variants aren't available for purchase
if (!value.stock.available) {
//CODE FOR DOUBLE ATTRIBUTE VARIANTS
//check if the variants are double attribute
if (value.title.indexOf(',') > -1) {
console.log('Double Attribute matrix!');
var attribute1 = value.title.replace(/"/g,'').split(',')[0].split(": ")[1];
//only disable the variants for which the first attribute is being viewed
if ($('select[name*="matrix"]:first()').val() == attribute1) {
var option = value.title.replace(/"/g,'').split(',')[1].split(":")[1];
$('select[name*="matrix"] option:contains(' + option + ')').each(function(){
if ($(this).text() == option) {
$(this).attr('disabled', 'true');
}
});
}
//CODE FOR SINGLE ATTRIBUTE VARIANTS
} else {
console.log('Single Attribute matrix!');
var option = value.title.split(': ')[1];
var selectname = value.title.split(': ')[0];
$('select[name*="matrix"] option:contains(' + option + ')').each(function(){
if ($(this).text() == option) {
$(this).attr('disabled', 'true');
}
});
}
}
})
});
} else {
console.log('not a product page!');
}
`
I'm trying to put a JSON object "Synced" (Which you will see in the code)
This is the code for a function "addServer(userid, serverid)"
The function is being required from another javascript file
db.all(`SELECT * FROM Users WHERE Tag = ? LIMIT 1`, userid, async(error,element) => {
if(element[0].Synced === '') {
var sJSON = {
users:{
[userid]:4,
},
servers:[`${serverid}`]
}
var serverJSON = JSON.stringify(sJSON)
console.log(serverJSON)
} else {
//Else statement not done yet
}
db.run(`UPDATE Users SET Synced = "${serverJSON}" WHERE Tag = "${userid}"`)
})
Solved. Needed to change quoting.
As Dave Newton said, I had to check my quoting. What I did was change my double quotes to single quotes which solved the problem.
I found an important security fault in my meteor app regarding subscriptions (maybe methods are also affected by this).
Even though I use the check package and check() assuring that the correct parameters data types are received inside the publication, I have realised that if a user maliciously subscribes to that subscription with wrong parameter data types it is affecting all other users that are using the same subscription because the meteor server is not running the publication while the malicious user is using incorrect parameters.
How can I prevent this?
Packages used:
aldeed:collection2-core#2.0.1
audit-argument-checks#1.0.7
mdg:validated-method
and npm
import { check, Match } from 'meteor/check';
Server side:
Meteor.publish('postersPub', function postersPub(params) {
check(params, {
size: String,
section: String,
});
return Posters.find({
section: params.section,
size: params.size === 'large' ? 'large' : 'small',
}, {
// fields: { ... }
// sort: { ... }
});
});
Client side:
// in the template:
Meteor.subscribe('postersPub', { size: 'large', section: 'movies' });
// Malicious user in the browser console:
Meteor.subscribe('postersPub', { size: undefined, section: '' });
Problem: The malicious user subscription is preventing all other users of getting answer from their postersPub subscriptions.
Extra note: I've also tried wrapping the check block AND the whole publication with a try catch and it doesn't change the effect. The error disappears from the server console, but the other users keep being affected and not getting data from the subscription that the malicious user is affecting.
Check method and empty strings
There is one thing to know about check and strings which is, that it accepts empty strings like '' which you basically showed in your malicious example.
Without guarantee to solve your publication issue I can at least suggest you to modify your check code and include a check for non-empty Strings.
A possible approach could be:
import { check, Match } from 'meteor/check';
const nonEmptyString = Match.Where(str => typeof str === 'string' && str.length > 0);
which then can be used in check like so:
check(params, {
size: nonEmptyString,
section: nonEmptyString,
});
Even more strict checks
You may be even stricter with accepted parameters and reduce them to a subset of valid entries. For example:
const sizes = ['large', 'small'];
const nonEmptyString = str => typeof str === 'string' && str.length > 0;
const validSize = str => nonEmptyString(str) && sizes.indexOf( str) > -1;
check(params, {
size: Match.Where(validSize),
section: Match.Where(nonEmptyString),
});
Note, that this also helps you to avoid query logic based on the parameter. You can change the following code
const posters = Posters.find({
section: params.section,
size: params.size === 'large' ? 'large' : 'small',
}, {
// fields: { ... }
// sort: { ... }
});
to
const posters = Posters.find({
section: params.section,
size: params.size,
}, {
// fields: { ... }
// sort: { ... }
});
because the method does anyway accept only one of large or small as parameters.
Fallback on undefined cursors in publications
Another pattern that can support you preventing publication errors is to call this.ready() if the collection returned no cursor (for whatever reason, better is to write good tests to prevent you from these cases).
const posters = Posters.find({
section: params.section,
size: params.size === 'large' ? 'large' : 'small',
}, {
// fields: { ... }
// sort: { ... }
});
// if we have a cursor with count
if (posters && posters.count && posters.count() >= 0)
return posters;
// else signal the subscription
// that we are ready
this.ready();
Combined code example
Applying all of the above mentioned pattern would make your function look like this:
import { check, Match } from 'meteor/check';
const sizes = ['large', 'small'];
const nonEmptyString = str => typeof str === 'string' && str.length > 0;
const validSize = str => nonEmptyString(str) && sizes.indexOf( str) > -1;
Meteor.publish('postersPub', function postersPub(params) {
check(params, {
size: Match.Where(validSize),
section: Match.Where(nonEmptyString),
});
const posters = Posters.find({
section: params.section,
size: params.size,
}, {
// fields: { ... }
// sort: { ... }
});
// if we have a cursor with count
if (posters && posters.count && posters.count() >= 0)
return posters;
// else signal the subscription
// that we are ready
this.ready();
});
Summary
I for myself found that with good check matches and this.ready() the problems with publications have been reduced to a minimum in my applications.
Developing web app with node.js and express.
I have following two urls to distinguish:
/api/v1/source?id=122323
/api/v1/source?timestamp=1555050505&count=10
I come up a naive solution. I leave such similar urls to one route method and use if eles to specify solutions, i.e:
if(id){
//solution with id
}
if(timestamp&&count){
//solution with timestamp and count but without id
}
Apparently, this is not clean. Because in the future,I may want to add new field which will make this router huge and ugly.
So How can I overcome this? Or to change url structure.I want to build a Restful api.
Try to put together all the properties in a list and use Array#every to check if all the values in Array evaluates to true.
Maybe something like this:
(( /* req, res */)=>{
// Dummy express Request Object
const req = {
params : {
//id : '123',
count : 10,
timestamp : 1555050505,
newParameter : 'whatever value'
}
}
let { params } = req;
let {
id
, count
, timestamp
, newParameter
} = params;
if(id){
console.log('Action with id');
return;
}
let secondConditionArray = [
count, timestamp, newParameter
];
if( secondConditionArray.every(Boolean) ){
console.log('Second Action')
} else {
console.log('Some values are no truthy')
}
})()
You can get Url parameters with req.params
if(req.params.id){
//solution with id
}
if(req.params.timestamp && req.params.count){
//solution with timestamp and count but without id
}
Can I allow the domain matching for my extension to be user configurable?
I'd like to let my users choose when the extension runs.
To implement customizable "match patterns" for content scripts, the Content script need to be executed in by the background page using the chrome.tabs.executeScript method (after detecting a page load using the chrome.tabs.onUpdated event listener).
Because the match pattern check is not exposed in any API, you have to create the method yourself. It is implemented in url_pattern.cc, and the specification is available at match patterns.
Here's an example of a parser:
/**
* #param String input A match pattern
* #returns null if input is invalid
* #returns String to be passed to the RegExp constructor */
function parse_match_pattern(input) {
if (typeof input !== 'string') return null;
var match_pattern = '(?:^'
, regEscape = function(s) {return s.replace(/[[^$.|?*+(){}\\]/g, '\\$&');}
, result = /^(\*|https?|file|ftp|chrome-extension):\/\//.exec(input);
// Parse scheme
if (!result) return null;
input = input.substr(result[0].length);
match_pattern += result[1] === '*' ? 'https?://' : result[1] + '://';
// Parse host if scheme is not `file`
if (result[1] !== 'file') {
if (!(result = /^(?:\*|(\*\.)?([^\/*]+))(?=\/)/.exec(input))) return null;
input = input.substr(result[0].length);
if (result[0] === '*') { // host is '*'
match_pattern += '[^/]+';
} else {
if (result[1]) { // Subdomain wildcard exists
match_pattern += '(?:[^/]+\\.)?';
}
// Append host (escape special regex characters)
match_pattern += regEscape(result[2]);
}
}
// Add remainder (path)
match_pattern += input.split('*').map(regEscape).join('.*');
match_pattern += '$)';
return match_pattern;
}
Example: Run content script on pages which match the pattern
In the example below, the array is hard-coded. In practice, you would store the match patterns in an array using localStorage or chrome.storage.
// Example: Parse a list of match patterns:
var patterns = ['*://*/*', '*exampleofinvalid*', 'file://*'];
// Parse list and filter(exclude) invalid match patterns
var parsed = patterns.map(parse_match_pattern)
.filter(function(pattern){return pattern !== null});
// Create pattern for validation:
var pattern = new RegExp(parsed.join('|'));
// Example of filtering:
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if (changeInfo.status === 'complete') {
var url = tab.url.split('#')[0]; // Exclude URL fragments
if (pattern.test(url)) {
chrome.tabs.executeScript(tabId, {
file: 'contentscript.js'
// or: code: '<JavaScript code here>'
// Other valid options: allFrames, runAt
});
}
}
});
To get this to work, you need to request the following permissions in the manifest file:
"tabs" - To enable the necessary tabs API.
"<all_urls>" - To be able to use chrome.tabs.executeScript to execute a content script in a specific page.
A fixed list of permissions
If the set of match patterns is fixed (ie. the user cannot define new ones, only toggle patterns), "<all_urls>" can be replaced with this set of permissions. You may even use optional permissions to reduce the initial number of requested permissions (clearly explained in the documentation of chrome.permissions).