security audit feedback
This commit is contained in:
@@ -14,8 +14,8 @@ cd ..
|
||||
cp -r instance1/ instance2
|
||||
sed -i "s/biggubis.benetou.fr/localhost:8081/" ./instance1/pages/federated.json
|
||||
sed -i "s/biggubis.benetou.fr/localhost:8080/" ./instance2/pages/federated.json
|
||||
cd instance1 && NODE_TLS_REJECT_UNAUTHORIZED=0 HTTPS=true PORT=8080 npm run start &
|
||||
cd instance2 && NODE_TLS_REJECT_UNAUTHORIZED=0 HTTPS=true PORT=8081 npm run start &
|
||||
cd instance1 && NODE_TLS_REJECT_UNAUTHORIZED=0 WORKSHOP=true HTTPS=true PORT=8080 npm run start &
|
||||
cd instance2 && NODE_TLS_REJECT_UNAUTHORIZED=0 WORKSHOP=true HTTPS=true PORT=8081 npm run start &
|
||||
|
||||
# for federated content itself, as CLI https://git.benetou.fr/utopiah/biggu_s_gate/src/branch/main/pages/remixes.js#L142-L154
|
||||
# could test verify with jq if getting proper JSON result
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
"version": "0.8.2",
|
||||
"description": "Biggu's Gate - Federating pedagogical immersive experiences",
|
||||
"scripts": {
|
||||
"get_and_install_assets": "cd pages && wget https://biggu.benetou.fr/assets.tar && tar -xvf assets.tar",
|
||||
"get_and_install_assets": "cd pages && wget https://biggu.benetou.fr/assets.tar && sha256sum -c ../checksums.txt && tar -xvf assets.tar",
|
||||
"generate_certificate": "openssl req -x509 -newkey rsa:4096 -keyout ./server/key.pem -out ./server/cert.pem -sha256 -days 3650 -nodes -subj '/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname'",
|
||||
"start": "node ./server/easyrtc-server.js",
|
||||
"workshop": "HTTPS=true WORKSHOP=true node ./server/easyrtc-server.js",
|
||||
"server_test": "curl localhost:8080/commands | jq",
|
||||
"server_federatrion_test": "bash ./documentation/local_federation_test",
|
||||
"server_and_domain_test": "curl $DOMAIN/federated | jq"
|
||||
@@ -13,6 +14,8 @@
|
||||
"author": "Fabien Benetou",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.6",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
"helmet": "^8.2.0",
|
||||
"multer": "^2.1.1",
|
||||
"networked-aframe": "^0.14.0",
|
||||
"open-easyrtc": "2.1.4"
|
||||
|
||||
@@ -111,7 +111,14 @@ let protocolInterfaces = []
|
||||
function onConnect() {
|
||||
console.log('onConnect', new Date());
|
||||
function applyMods(a,b,c,d){ console.log(c); eval(c) }
|
||||
NAF.connection.subscribeToDataChannel('modifications', applyMods)
|
||||
fetch('/workshop').then( res => res.json() ).then( res => {
|
||||
if (res) {
|
||||
NAF.connection.subscribeToDataChannel('modifications', applyMods)
|
||||
console.log('trusted environment, namely workshop mode, so enabling evaluation of code')
|
||||
} else {
|
||||
console.log('untrusted environment, disabling remote evaluation of code')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const loadremix = urlParams.get('loadRemix');
|
||||
@@ -259,7 +266,7 @@ function rand(items) {
|
||||
}
|
||||
|
||||
var room = urlParams.get('room');
|
||||
if (!room) room = "remixed"
|
||||
if (!room) room = "remixed_room_of_" + nickname
|
||||
document.querySelector("a-scene").setAttribute("networked-scene", "room: "+room+"; debug: true; adapter: wseasyrtc;")
|
||||
// testable as https://localhost/biggu_s_gate/remixed_room.html?room=other&loadRemix=remix__blue_fish_blue_1769752869988.json
|
||||
// consider player-info or nametag examples
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
const helmet = require("helmet");
|
||||
const rateLimit = require("express-rate-limit");
|
||||
|
||||
const fs = require("fs");
|
||||
const http = require("https");
|
||||
const path = require("path");
|
||||
@@ -23,6 +26,24 @@ const upload = multer({
|
||||
})
|
||||
|
||||
module.exports = function(app, express){
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
limit: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
ipv6Subnet: 56, // Set to 60 or 64 to be less aggressive, or 52 or 48 to be more aggressive
|
||||
})
|
||||
if ( !process.env.WORKSHOP ){
|
||||
console.log( 'workshop mode not detected, applying helmet and rate limiter' )
|
||||
app.use(helmet())
|
||||
app.use( limiter )
|
||||
app.get('/workshop', (req, res) => res.json( false ) )
|
||||
// preventing NAF from enabling eval()
|
||||
} else {
|
||||
console.log( 'workshop mode detected, disabling helmet and rate limiter (WARNING: be cautious, LAN only!)' )
|
||||
app.get('/workshop', (req, res) => res.json( true ) )
|
||||
}
|
||||
|
||||
const defaultProtocol = 'https://' // could be via another but for now default
|
||||
|
||||
// if adminToken exists, if so use it
|
||||
@@ -183,33 +204,58 @@ module.exports = function(app, express){
|
||||
})
|
||||
|
||||
app.post('/icosasave/', (req, res) => {
|
||||
const icosaWhitelist = [
|
||||
'https://api.icosa.gallery/',
|
||||
'http://api.icosa.gallery/', // seems to sometimes fallback to HTTP which isn't great
|
||||
] // self-hosted galleries would be added here
|
||||
|
||||
// should also check req.body.data.startsWith( 'https://web.archive.org/' ) // but there are a couple more legitimate ones
|
||||
// also this could still host malicious content
|
||||
let matches = icosaWhitelist.filter( wl => req.body.metadata.url.startsWith( wl )).length
|
||||
if ( matches == 0 ){
|
||||
console.log('non-whitelisted Icosa gallery server', req.body.metadata.url );
|
||||
return res.status(401).json({ error: 'non-whitelisted Icosa gallery server, contact the administrator' });
|
||||
}
|
||||
// TODO an caching index would help, the files are small but still there is no need to download again duplicates
|
||||
const uniqfilename = req.body.metadata.assetId + '.glb' // sticking to assetId from Icosa
|
||||
|
||||
fs.writeFileSync(path.resolve(assetsFromIcosaPath,uniqfilename + '.metadata.json'), JSON.stringify(req.body.metadata) )
|
||||
// security issue, trust it be genuinely be JSON, should be verifying and whitelisting fields
|
||||
|
||||
wget( req.body.data, assetsFromIcosaPath + '/' + uniqfilename ) .then( wgetres => {
|
||||
console.log( wgetres )
|
||||
return res.json( uniqfilename )
|
||||
console.log( wgetres ) // never getting here, is the promise really resolving?
|
||||
// pushing verification there
|
||||
})
|
||||
//return res.status(401)
|
||||
return res.json( uniqfilename )
|
||||
/*
|
||||
if ( ! verifyFileViaHeader( assetsFromIcosaPath + '/' + uniqfilename ) ){
|
||||
// should discard the file too
|
||||
console.log( 'non glTF file' )
|
||||
return res.status(401).json({ error: 'non glTF content' });
|
||||
}
|
||||
console.log( 'verified glTF file' ) // never getting here either
|
||||
*/
|
||||
return res.json( uniqfilename ) // yet somehow returning the right value or rather assumption that it did work
|
||||
})
|
||||
|
||||
app.get('/federated/added/by/:source', (req, res) => {
|
||||
let otherServerBaseURL = req.params.source
|
||||
if ( otherServerBaseURL.endsWith('_federated') ) {
|
||||
fetch(defaultProtocol + otherServerBaseURL.replace('_','/') ).then( res => {
|
||||
if (res.status == 200)
|
||||
res.json()
|
||||
else
|
||||
return res.status(401).json({ error: 'unreachable source of federating server' });
|
||||
} ).then( json => {
|
||||
console.log( 'new federation remotely from https://', otherServerBaseURL )
|
||||
res.json( true )
|
||||
}).catch((error) => {
|
||||
console.log(error)
|
||||
return res.status(401).json({ error: 'unreachable source of federating server' + error });
|
||||
});
|
||||
if ( process.env.WORKSHOP ){
|
||||
// only fetching content in a trusted environment
|
||||
fetch(defaultProtocol + otherServerBaseURL.replace('_','/') ).then( res => {
|
||||
if (res.status == 200)
|
||||
res.json()
|
||||
else
|
||||
return res.status(401).json({ error: 'unreachable source of federating server' });
|
||||
} ).then( json => {
|
||||
res.json( true )
|
||||
}).catch((error) => {
|
||||
console.log(error)
|
||||
return res.status(401).json({ error: 'unreachable source of federating server' + error });
|
||||
});
|
||||
} // otherwise too dangerous
|
||||
console.log( 'new federation attempt remotely from https://' + otherServerBaseURL + ' (visit with caution, consider checking whitelists first)' )
|
||||
res.json( false )
|
||||
} else {
|
||||
return res.status(401).json({ error: 'badly formatted federation registration! _federated should end the query' });
|
||||
}
|
||||
@@ -297,21 +343,35 @@ module.exports = function(app, express){
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function wget(url, dest) {
|
||||
return new Promise((res) => {
|
||||
http.get(url, (response) => {
|
||||
if (response.statusCode == 302) {
|
||||
// if the response is a redirection, we call again the method with the new location
|
||||
wget(String(response.headers.location), dest);
|
||||
} else {
|
||||
const file = fs.createWriteStream(dest);
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(url, (response) => { // can be https, depends on initial import
|
||||
if (response.statusCode == 302) {
|
||||
// if the response is a redirection, we call again the method with the new location
|
||||
wget(String(response.headers.location), dest)
|
||||
} else {
|
||||
const file = fs.createWriteStream(dest)
|
||||
|
||||
response.pipe(file);
|
||||
file.on("finish", function () {
|
||||
file.close();
|
||||
res();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
response.pipe(file)
|
||||
file.on("finish", function () {
|
||||
file.close()
|
||||
resolve()
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verifyFileViaHeader( filename, stringToVerify='glTF', maxLength = 4){
|
||||
fs.open(filename, 'r', function(status, fd) {
|
||||
if (status) {
|
||||
console.log( status )
|
||||
return status.message // error situation
|
||||
}
|
||||
var buffer = Buffer.alloc(maxLength);
|
||||
fs.read(fd, buffer, 0, maxLength, 0, function(err, num) {
|
||||
const headerish = buffer.toString('utf8', 0, num)
|
||||
console.log( headerish, headerish == stringToVerify )
|
||||
return headerish == stringToVerify
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,7 +85,8 @@ const myIceServers = [
|
||||
// }
|
||||
];
|
||||
easyrtc.setOption("appIceServers", myIceServers);
|
||||
easyrtc.setOption("logLevel", "debug");
|
||||
easyrtc.setOption("logLevel", "warning");
|
||||
// see https://github.com/open-easyrtc/open-easyrtc/blob/develop/docs/easyrtc_server_configuration.md
|
||||
easyrtc.setOption("demosEnable", false);
|
||||
|
||||
// Overriding the default easyrtcAuth listener, only so we can directly access its callback
|
||||
|
||||
Reference in New Issue
Block a user