HMAC Signature doesn't match x-hub-signature from github - javascript

I'm handling an incoming Webhook from github, and wants to verify the x-hub-signature. I'm using hmacto hash the "secret", and then compares the two hashes. The problem is that they never match. This is my setup:
router.route("/auth")
.post((req, res) => {
var hmac = crypto.createHmac("sha1", process.env.WEBHOOK_SECRET);
var calculatedSignature = "sha1=" + hmac.update(JSON.stringify(req.body)).digest("hex");
console.log(req.headers["x-hub-signature"] === calculatedSignature); // Returns false
console.log(req.headers["x-hub-signature"]) // => sha1=blablabla
console.log(calculatedSignature) // => sha1=foofoofoo
res.end();
});
I've tried everything, but can't make it work. Wondering if the hmac.update() should hold another parameter than JSON.stringify(req.body). Does anyone know why they won't match?

So the problem was with the settings of the webhook. The content-format was set to application/x-www-form-urlencoded, which for some reason hashed the x-hub-signature differently. I just changed it to application/json, and then it worked!

If the webhook Content-Type is set to application/x-www-url-encoded then string you need to use to check the HMAC is
"payload=" + query_encoded_payload.
For example in golang
payloadForm := r.PostFormValue("payload")
escaped := url.QueryEscape(payloadForm) # ex. http://www.url-encode-decode.com/
checkMe := "payload=" + escaped

Related

How to use bcrypt to encrypt a date and decrypt it for comparison?

I am sending a token to user email like below and using bcrypt for this as an encrypt/decrypt mechanism.
const token = await bcrypt.hash(joinDate, 10);
When the user clicks on the link in email, I get the above token back as that token is a part of
/api/unsubscribe?userId="abcd"&token="token_that_was_generated_using_bcrypt_and_sent_to_user"
const {userId, token} = req.query;
In the api, I am comparing joinDate obtained from database vs token sent by req.query but it never matches.
const joinDate = user.joinDate.toString();
const tokenValidated = await bcrypt.compare(joinDate, token)//this is always false although us generated from same joinDate field
Why is tokenValidated always false although it was generated using the same field joinDate?
Your use of bcrypt is not secure. Anyone can brute-force a few hundred or a few thousand dates and cause any user to become unsubscribed.
I assume your motivation is to avoid having to create a new table in your database and store a random token for every email you've sent out.
If so, the proper tool to use is HMAC.
Your URL should be of the form: /api/unsubscribe?userId=abcd&mac=....
Come up with a secret key known only to your server. This secret key will be used to create and authenticate all unsubscribe requests. Only use this key for authenticating unsubscribe requests.
Perform HMAC-SHA512 on the user id, with the HMAC output truncated to 128 bits. Then base64-encode the 128 bits and set it as the mac parameter in the URL.
HMAC means to create a hash-based message authentication code, which will confirm to your server that it must have created and emailed out that 'mac'.
Now, your server can authenticate each response, because only someone with knowledge of the server's secret key can produce a valid unsubscribe link.
Your logic of using bcrypt for hash and verifying on the other end should work. The only possible thing that can prevent it from working is either:
1 - the joinDate here: const token = await bcrypt.hash(joinDate, 10);
is not ===
to joinDate here:const tokenValidated = await bcrypt.compare(joinDate, token)
( maybe differes in ", ' or different format orsomething )
2 - or the token is kinda different in ' or " when passing through / read from queryparam; (e.g. you token is going "'token_that_was_generated_using_bcrypt_and_sent_to_user'" for comparison.)

Can you tell me if express-mongo-sanitize is working?

I'm trying to set up some security middleware for my humble little MERN web app, and I'm currently using helmet and express-mongo-sanitize, specifically for protection against NoSQL injection attacks.
I've set it up, however, as below in my server.js file:
const express = require('express')
const helmet = require('helmet')
const mongoSanitize = require('express-mongo-sanitize')
...
app.use(mongoSanitize())
app.use(helmet())
// Routes below
...
I've tried to test it by making a mock sign up like:
username: {"$gt": ""}
password: 'TestPassword'
so that req.body would be:
{
username: '{"$gt": ""}',
password: 'TestPassword'
}
but express-mongo-sanitize doesn't seem to be catching it and it goes through to my database. Am I misunderstanding something? The value of the username key is a string, so maybe it's already OK? Please forgive my ignorance, I'm learning.
What express-mongo-sanitize does is sanitize keys that start with a dollar sign.
username: '{"$gt": ""}' --> this isn't a key starting with a dollar sign. Rather, the value of username is just a string.
Try sending it this object instead:
{
"username": { "$gt": "" }
}
From what I understood, from debugging and going through the code, the keys it sanitizes is any potential key in a key=value pair of query & post params that have the $ or a dot. It also tries to sanitize any keys in the body and header of the request.
For example, even the json provided by the previous user above won't do it.
but https://your-domain/?$user=json would be sanitized to user=json.
It doesn't remove $ from the value of the params as you and also I was expecting. I also opened up a question on the github to the creator and will see what he says. I would think the security risk is for both the key and value. This doesn't do any good if you're not saving the key to mongodb but saving the value instead.
For reference it checks the following HTTP sections to remove any potential harmful $ or .
['body', 'params', 'headers', 'query'].forEach(function (key) ...
I think the answer by #A10n is correct, and so it appears that 'express-mongo-sanitize' is only a partial solution. I've added my quick-and-dirty solution to remove curly brackets from every req.body/params object (which should blunt most NoSQL attacks) like so:
// Middleware that replaces all {} symbols with [] to prevent NoSQL attack
const removeCurlies = (req, res, next) => {
Object.keys(req.body).forEach((key) => {
req.body[key] = _.replace(req.body[key], "{", "[");
req.body[key] = _.replace(req.body[key], "}", "]");
});
Object.keys(req.params).forEach((key) => {
req.params[key] = _.replace(req.params[key], "{", "[");
req.params[key] = _.replace(req.params[key], "}", "]");
});
next();
};
Then, before your routes:
app.use(removeCurlies);
Ran into the same problem and what worked for me was parsing the incoming request body before using the mongoSanitize package. Like this...
const express = require("express")
const mongoSanitize = require("express-mongo-sanitize')
const app = express()
app.use(express.json())
app.use(mongoSanitize())

Verify Stripe web-hook manually

I am trying to manually verify web-hook:
const stripeSecret = createHmac('sha256', STRIPE_SIGNING_SECRET)
.update(event.body)
.digest('hex');
if(stripeSecret !== keyFromHeader) {
throw err();
}
But it is not matched with Stripe secret key which is received in header.
Here is event data which I am also trying to use in Stripe API (it also fails):
it's not event.body you should hash According to the documentation (https://stripe.com/docs/webhooks/signatures#verify-manually
) its a concatenation of :
The timestamp (as a string)
The character .
The actual JSON payload (i.e., the request body) => JSON.stringify(req.body)
you will need to parse this to get the timestamp (the xxxxx in the "t=xxxxx" part)
const sig = request.headers['stripe-signature'];
if you give me a sample stripe signature header i can try a code sample.
event.body might not be enough — it's very common in Node server environments for that to be a "parsed" version of the incoming request body, and instead you need to make sure to access the actual raw JSON string in the body — that's what Stripe's signature is computed against. It can be a little tricky!
https://github.com/stripe/stripe-node/tree/master/examples/webhook-signing
(and many examples contributed for various frameworks at https://github.com/stripe/stripe-node/issues/341 )
Also, is there a specific reason to do this manually and not just use Stripe's Node library? :)

Generate jwt web auth tokens in ember-cli-mirage (or any JavaScript) for use in your ember app

I work on an Ember team that implemented djangorestframework-simplejwt for our API security. It's a good API solution, but our mirage user was getting logged out after a period of time and could not log back into our app (for testing, development). I traced the problem down to how jwt works, and the fact that I had pasted static jwt tokens in our mirage config /login endpoint.
jwt or JSON Web Tokens contain an expiration date, set on the server. Once that expiration date passes, the client cannot be auth'ed into the app anymore, until the server sends a new token with a future expiration date. This was a problem for our mirage ENV, because the mirage endpoint for /login was returning a static jwt token which I had copy/pasted from our backend response. The workaround was to get new tokens from our backend, paste them into our mirage config and use them until they expire, which is not a true permanent solution to this problem.
After a LOT of trial and error (and learning way too much about jwt), I came up with this solution, which creates a valid jwt token with an expiration date 7 days in the future. It only requires crypto-js (npm install crypto-js), a very lightweight library with many crypto functions, but no dependencies:
import CryptoJS from 'CryptoJS';
const generateTokens = function(secretKey) { // fn to generate jwt access and refresh tokens with live date (+7 days) expiration
let newEpochDate = new Date().valueOf();
newEpochDate += 6.048e8; // add 7 days padding
newEpochDate = Math.trunc(newEpochDate / 1000); // convert to Java epoch date value
let tokenObjBase = {
'typ': 'JWT',
'alg': 'HS256'
};
let tokenObjAccess = {
'token_type': 'access',
'exp': newEpochDate,
'jti': '83bc20a2fb564aa8937d167586166f67',
'user_id': 24865
};
let tokenObjRefresh = {
'token_type': 'refresh',
'exp': newEpochDate,
'jti': '83bc20a2fb564aa8937d167586166f67',
'user_id': 24865
};
let base64urlEncode = function (obj) {
let base64url = CryptoJS.enc.Utf8.parse(JSON.stringify(obj)).toString(CryptoJS.enc.Base64);
base64url = base64url.replace(/=/g, '').replace(/\//g, '_').replace(/\+/g, '-'); // crypto-js doesn't have base64url encoding; we must manually make the tokens URL safe
return base64url;
}
let tokenBase = base64urlEncode(tokenObjBase);
let tokenAccess = base64urlEncode(tokenObjAccess);
let tokenRefresh = base64urlEncode(tokenObjRefresh);
let signatureAccessArray = CryptoJS.HmacSHA256(tokenBase + '.' + tokenAccess, secretKey); // crypto-js returns a "wordarray" which must be stringified back to human readable text with a specific encoding
let signatureAccess = signatureAccessArray.toString(CryptoJS.enc.Base64).replace(/=+$/, '').replace(/\//g, '_').replace(/\+/g, '-'); // crypto-js doesn't have base64url encoding; we must manually make the tokens URL safe
let signatureRefreshArray = CryptoJS.HmacSHA256(tokenBase + '.' + tokenRefresh, secretKey);
let signatureRefresh = signatureRefreshArray.toString(CryptoJS.enc.Base64).replace(/=+$/, '').replace(/\//g, '_').replace(/\+/g, '-'); // crypto-js doesn't have base64url encoding; we must manually make the tokens URL safe
return {tokenRefresh: tokenBase + '.' + tokenRefresh + '.' + signatureRefresh, tokenAccess: tokenBase + '.' + tokenAccess + '.' + signatureAccess};
}
export default function() { // ...rest of mirage/config.js
// you may also need this in your ember-cli-build:
app.import('node_modules/crypto-js/crypto-js.js', {
using: [
{ transformation: 'amd', as: 'CryptoJS' }
]
});
This fn can be called by any route in the config file: let tokens = generateTokens('thisisnotarealsecretkey');
It returns an object with an "access" token and a "refresh" token, the two token types required by our django jwt setup. Customize the tokenObjBase, tokenObjAccess and tokenObjRefresh to meet your backend's setup.
The basic structure of a jwt token can be found here: https://jwt.io/
To summarize, a jwt token has three strings, separated by two periods (.).
The first string is the tokenObjBase passed through JSON.stringify(), then converted to a base64URL value. That URL part is important, because regular base64 encodings don't remove the =, + and / chars, which are not "web safe." The tokenObjBase must contain typ and alg properties and nothing else.
The second string is your "payload" (here, tokenObjAccess or tokenObjRefresh) and usually contains user info (name, id, etc), and also an epoch date value which represents the expiration date of the token. That payload obj, like the first, is passed through JSON.stringify(), then converted to a base64URL value. DO NOT put sensitive data in these first two objs, they are not "encrypted" at all. Base64 encoding can be reversed by anyone with a computer and Google.
The third string is the jwt "signature." It is created by concatenating the first two base64 strings with a period (.) in the middle, then passing them through the HS256 encryption algorithm (HMAC-SHA256).
Then all three strings (two base64URL strings and the HS256 encrypted string) are concatenated: base64URL(tokenObjBase) + '.' + base64URL(tokenObjPayload) + '.' + signatureHS256
Hope this helps anyone having issues with jwt permanently logging their mirage users out of their Ember applications!

Facebook messenger API incomplete content in request body

The explanation is a bit long so please bear with me.
I am building a Facebook messenger bot which uses my sails.js/node.js server in the backend and a MongoDB database.
In my sails app, I have applied policies to the method of the controller which handles the operations to be performed after recieving a text from the user. In this policy, I am following the documentation(https://developers.facebook.com/docs/messenger-platform/webhook-reference - "Security" section) and comparing the x-hub-signature that comes in the request's header with the sha1 digest of the request payload(body).
So now whenever I am sending a message to the bot, it says in the policy that the signature from the request and the signature calculated by me is different and thus, doesnt go further. I double checked the app secret which I should use while calculating the digest and it seems to be correct. Another difference which I found was that, Facebook request also sends a "content-length" field in its header, which is different than character length of the body they sent in the same request. And this is what I think is the reason for different signatures but I am unable to resolve it and get to the root of the problem as to why is this happening.
Also another thing to note is that the same code that throws this mismatch error, runs perfectly at certain times(actually, most of the times).
So can somebody please help me this? I ll be forever grateful :)
Here is the code from the policy
var crypto = require('crypto');
if(req.headers['x-hub-signature']){
//console.log('req headers -----', JSON.stringify(req.headers));
//console.log('req body -----', JSON.stringify(req.body));
var hmac, calculatedSignature, payload = req.body;
hmac = crypto.createHmac('sha1', app_secret);
hmac.update(JSON.stringify(payload));
calculatedSignature = 'sha1='+hmac.digest('hex');
//console.log("signature calculatedSignature",calculatedSignature);
if(calculatedSignature === req.headers['x-hub-signature']){
return next();
}else{
res.forbidden('You shall not pass!');
}
}
This is a sample request header -
{"host":"e93d4245id.ngrok.io","accept":"*/*","accept-encoding":"deflate, gzip","content-type":"application/json","x-hub-signature":"sha1=d0cd8177add9b1ff367d411942603b0d08183964","content-length":"274","x-forwarded-proto":"https","x-forwarded-for":"127.0.0.1"}
And this is the body from the same request -
{"object":"page","entry":[{"id":"1778585282425767","time":1479476014038,"messaging":[{"sender":{"id":"userId"},"recipient":{"id":"recipientId"},"timestamp":1479468097895,"message":{"mid":"mid.1479468097895:efdc7d2c68","seq":2355,"text":"Hahahaha"}}]}]}
I think the problem was some specific characters such as # and % were needed to be converted to their unicode escape sequence as specified in their documentation and replaced in the original stringified JSON. I converted them and then calculated the hmac signature of the new string and it got matched.
Also the reason why it was working and why it was not in certain cases was I think because of the special characters being present in that string which was being stringified. If it did not have the characters # or % then it worked without any problem.
This is how I solved it -
inside if
var hmac, calculatedSignature, payload = JSON.stringify(req.body);
var resStr = payload.replace(/\#|\%/g,function(a, i){
hex = payload.charCodeAt(i).toString(16);
var s = "\\u" + ("000"+hex).slice(-4);
return s;
});
hmac = crypto.createHmac('sha1', app_secret);
hmac.update(resStr);
calculatedSignature = 'sha1='+hmac.digest('hex');
if(calculatedSignature === req.headers['x-hub-signature']){
return next();
}else{
res.forbidden('You shall not pass!');
}
Your bodyParserJSON should return rawBody (just stringifying will fail in many cases):
bodyParser.json({
verify(req, res, buf) {
req.rawBody = buf;
},
})
Here is a middleware that I've written. It uses crypto module to generate sha1
fbWebhookAuth: (req, res, next) => {
const hmac = crypto.createHmac('sha1', process.env.FB_APP_SECRET);
hmac.update(req.rawBody, 'utf-8');
if (req.headers['x-hub-signature'] === `sha1=${hmac.digest('hex')}`) next();
else res.status(400).send('Invalid signature');
}
and finally in your route you can use it as:
app.post('/webhook/facebook', middlewares.fbWebhookAuth, facebook.webhook);

Categories