security audit feedback

This commit is contained in:
Utopiah
2026-07-01 09:56:14 +02:00
parent d6c885a28e
commit 87efccc4c0
5 changed files with 108 additions and 37 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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
})
})
}

View File

@@ -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