serveBlob: only serve blobs referenced by feeds we replicate
serveBlob handed any held blob to anyone who knew its hash, on a public port with require_opt_in:false. Combined with ssb-blobs sympathy that meant publicly serving 4.6GB of strangers content we never reviewed. Maintains a set of blob ids referenced by the local log (small, ~360KB, rescanned every 5 min) and 404s anything outside it. Fails open until the first scan completes so a read error cannot 404 the whole site. Verified: a blob present on disk but referenced by no message returns blobs.has=true over RPC while the viewer 404s it.
This commit is contained in:
@@ -44,6 +44,43 @@ exports.version = require('./package').version
|
||||
|
||||
exports.init = function (sbot, config) {
|
||||
var conf = config.viewer || {}
|
||||
|
||||
// --- known-blob gate -----------------------------------------------------
|
||||
// This node used to cache blobs on behalf of strangers (ssb-blobs `sympathy`
|
||||
// defaults to 3), and serveBlob would hand any of them to anyone who knew the
|
||||
// hash. sympathy is 0 now and the cache has been pruned, so everything we hold
|
||||
// is referenced by a feed we replicate. This keeps that true even if a stray
|
||||
// blob ever lands.
|
||||
//
|
||||
// The local log is small (~360KB), so a regex scan is cheap. It is refreshed
|
||||
// periodically rather than kept live: being a few minutes stale can only delay
|
||||
// a legitimate image, never serve one that is not ours.
|
||||
var knownBlobs = null // null = not loaded yet -> fail open, never 404 everything
|
||||
var logOffsetPath = path.join(config.path, 'flume', 'log.offset')
|
||||
|
||||
function refreshKnownBlobs() {
|
||||
fs.readFile(logOffsetPath, function (err, buf) {
|
||||
if (err) {
|
||||
console.error('[viewer] blob gate: could not read log.offset:', err.message)
|
||||
return // keep whatever set we already had
|
||||
}
|
||||
var found = buf.toString('binary').match(/&[A-Za-z0-9+/]{43}=\.sha256/g) || []
|
||||
var next = Object.create(null)
|
||||
found.forEach(function (id) { next[id] = true })
|
||||
if (knownBlobs === null) {
|
||||
console.log('[viewer] blob gate active:', Object.keys(next).length, 'known blobs')
|
||||
}
|
||||
knownBlobs = next
|
||||
})
|
||||
}
|
||||
refreshKnownBlobs()
|
||||
setInterval(refreshKnownBlobs, 5 * 60 * 1000).unref()
|
||||
|
||||
// serveBlob is a free function outside this closure, so hand the check over.
|
||||
sbot.isKnownBlob = function (id) {
|
||||
if (knownBlobs === null) return true
|
||||
return !!knownBlobs[id]
|
||||
}
|
||||
var port = conf.port || 8807
|
||||
var host = conf.host || config.host || '::'
|
||||
|
||||
@@ -474,6 +511,12 @@ function serveBlob(req, res, sbot, id, query) {
|
||||
var etag = id + (unbox || '')
|
||||
|
||||
if (req.headers['if-none-match'] === etag) return respond(res, 304)
|
||||
// Only serve blobs referenced by a feed we replicate. Anything else is not
|
||||
// ours to hand out, so treat it as absent rather than confirm we hold it.
|
||||
if (typeof sbot.isKnownBlob === 'function' && !sbot.isKnownBlob(id)) {
|
||||
return respond(res, 404, 'Not found')
|
||||
}
|
||||
|
||||
sbot.blobs.has(id, function (err, has) {
|
||||
if (err) {
|
||||
if (/^invalid/.test(err.message)) return respond(res, 400, err.message)
|
||||
|
||||
Reference in New Issue
Block a user