huge security fix

This commit is contained in:
IonutParau 2025-07-16 14:47:08 +02:00
parent 4a5562549c
commit 59eb01b890
5 changed files with 114 additions and 44 deletions

View File

@ -16,7 +16,6 @@
# Bugfixes
- Rework filesystem component to pre-process paths to ensure proper sandboxing and not allow arbitrary remote file access
- Do a huge audit at some point
- `nn_unicode_charWidth` appears to be bugged, look into that.

View File

@ -102,16 +102,6 @@ void *nn_fs_unwrapFD(nn_filesystem *fs, nn_size_t fd) {
return file;
}
nn_bool_t nn_fs_illegalPath(const char *path) {
// absolute disaster
const char *illegal = "\"\\:*?<>|";
for(nn_size_t i = 0; illegal[i] != '\0'; i++) {
if(nn_strchr(path, illegal[i]) != NULL) return true;
}
return false;
}
void nn_fs_readCost(nn_filesystem *fs, nn_size_t bytes, nn_component *component) {
nn_filesystemControl control = fs->control;
nn_computer *computer = nn_getComputerOfComponent(component);
@ -201,13 +191,14 @@ void nn_fs_size(nn_filesystem *fs, nn_componentMethod *_, nn_component *componen
nn_setCError(computer, "bad path (string expected)");
return;
}
if(nn_fs_illegalPath(path)) {
char canonical[NN_MAX_PATH];
if(nn_path_canonical(path, canonical)) {
nn_setCError(computer, "bad path (illegal path)");
return;
}
nn_lock(&fs->ctx, fs->lock);
nn_size_t byteSize = fs->table.size(fs->table.userdata, path);
nn_size_t byteSize = fs->table.size(fs->table.userdata, canonical);
nn_unlock(&fs->ctx, fs->lock);
nn_return(computer, nn_values_integer(byteSize));
@ -220,13 +211,14 @@ void nn_fs_remove(nn_filesystem *fs, nn_componentMethod *_, nn_component *compon
nn_setCError(computer, "bad path (string expected)");
return;
}
if(nn_fs_illegalPath(path)) {
char canonical[NN_MAX_PATH];
if(nn_path_canonical(path, canonical)) {
nn_setCError(computer, "bad path (illegal path)");
return;
}
nn_lock(&fs->ctx, fs->lock);
nn_size_t removed = fs->table.remove(fs->table.userdata, path);
nn_size_t removed = fs->table.remove(fs->table.userdata, canonical);
nn_unlock(&fs->ctx, fs->lock);
nn_return_boolean(computer, removed > 0);
@ -240,13 +232,14 @@ void nn_fs_lastModified(nn_filesystem *fs, nn_componentMethod *_, nn_component *
nn_setCError(computer, "bad path (string expected)");
return;
}
if(nn_fs_illegalPath(path)) {
char canonical[NN_MAX_PATH];
if(nn_path_canonical(path, canonical)) {
nn_setCError(computer, "bad path (illegal path)");
return;
}
nn_lock(&fs->ctx, fs->lock);
nn_size_t t = fs->table.lastModified(fs->table.userdata, path);
nn_size_t t = fs->table.lastModified(fs->table.userdata, canonical);
nn_unlock(&fs->ctx, fs->lock);
// OpenOS does BULLSHIT with this thing, dividing it by 1000 and expecting it to be
@ -264,7 +257,8 @@ void nn_fs_rename(nn_filesystem *fs, nn_componentMethod *_, nn_component *compon
nn_setCError(computer, "bad path #1 (string expected)");
return;
}
if(nn_fs_illegalPath(from)) {
char canonicalFrom[NN_MAX_PATH];
if(nn_path_canonical(from, canonicalFrom)) {
nn_setCError(computer, "bad path #1 (illegal path)");
return;
}
@ -275,13 +269,14 @@ void nn_fs_rename(nn_filesystem *fs, nn_componentMethod *_, nn_component *compon
nn_setCError(computer, "bad path #2 (string expected)");
return;
}
if(nn_fs_illegalPath(to)) {
char canonicalTo[NN_MAX_PATH];
if(nn_path_canonical(to, canonicalTo)) {
nn_setCError(computer, "bad path #2 (illegal path)");
return;
}
nn_lock(&fs->ctx, fs->lock);
nn_size_t movedCount = fs->table.rename(fs->table.userdata, from, to);
nn_size_t movedCount = fs->table.rename(fs->table.userdata, canonicalFrom, canonicalTo);
nn_unlock(&fs->ctx, fs->lock);
nn_return(computer, nn_values_boolean(movedCount > 0));
@ -296,13 +291,14 @@ void nn_fs_exists(nn_filesystem *fs, nn_componentMethod *_, nn_component *compon
nn_setCError(computer, "bad path (string expected)");
return;
}
if(nn_fs_illegalPath(path)) {
char canonical[NN_MAX_PATH];
if(nn_path_canonical(path, canonical)) {
nn_setCError(computer, "bad path (illegal path)");
return;
}
nn_lock(&fs->ctx, fs->lock);
nn_return_boolean(computer, fs->table.exists(fs->table.userdata, path));
nn_return_boolean(computer, fs->table.exists(fs->table.userdata, canonical));
nn_unlock(&fs->ctx, fs->lock);
}
@ -313,13 +309,14 @@ void nn_fs_isDirectory(nn_filesystem *fs, nn_componentMethod *_, nn_component *c
nn_setCError(computer, "bad path (string expected)");
return;
}
if(nn_fs_illegalPath(path)) {
char canonical[NN_MAX_PATH];
if(nn_path_canonical(path, canonical)) {
nn_setCError(computer, "bad path (illegal path)");
return;
}
nn_lock(&fs->ctx, fs->lock);
nn_return_boolean(computer, fs->table.isDirectory(fs->table.userdata, path));
nn_return_boolean(computer, fs->table.isDirectory(fs->table.userdata, canonical));
nn_unlock(&fs->ctx, fs->lock);
}
@ -330,13 +327,14 @@ void nn_fs_makeDirectory(nn_filesystem *fs, nn_componentMethod *_, nn_component
nn_setCError(computer, "bad path (string expected)");
return;
}
if(nn_fs_illegalPath(path)) {
char canonical[NN_MAX_PATH];
if(nn_path_canonical(path, canonical)) {
nn_setCError(computer, "bad path (illegal path)");
return;
}
nn_lock(&fs->ctx, fs->lock);
nn_return_boolean(computer, fs->table.makeDirectory(fs->table.userdata, path));
nn_return_boolean(computer, fs->table.makeDirectory(fs->table.userdata, canonical));
nn_unlock(&fs->ctx, fs->lock);
nn_fs_createCost(fs, 1, component);
@ -349,7 +347,8 @@ void nn_fs_list(nn_filesystem *fs, nn_componentMethod *_, nn_component *componen
nn_setCError(computer, "bad path (string expected)");
return;
}
if(nn_fs_illegalPath(path)) {
char canonical[NN_MAX_PATH];
if(nn_path_canonical(path, canonical)) {
nn_setCError(computer, "bad path (illegal path)");
return;
}
@ -358,7 +357,7 @@ void nn_fs_list(nn_filesystem *fs, nn_componentMethod *_, nn_component *componen
nn_size_t fileCount = 0;
nn_lock(&fs->ctx, fs->lock);
char **files = fs->table.list(alloc, fs->table.userdata, path, &fileCount);
char **files = fs->table.list(alloc, fs->table.userdata, canonical, &fileCount);
nn_unlock(&fs->ctx, fs->lock);
if(files != NULL) {
@ -380,7 +379,8 @@ void nn_fs_open(nn_filesystem *fs, nn_componentMethod *_, nn_component *componen
nn_setCError(computer, "bad path (string expected)");
return;
}
if(nn_fs_illegalPath(path)) {
char canonical[NN_MAX_PATH];
if(nn_path_canonical(path, canonical)) {
nn_setCError(computer, "bad path (illegal path)");
return;
}
@ -406,7 +406,7 @@ void nn_fs_open(nn_filesystem *fs, nn_componentMethod *_, nn_component *componen
return;
}
}
void *file = fs->table.open(fs->table.userdata, path, mode);
void *file = fs->table.open(fs->table.userdata, canonical, mode);
if(file == NULL) {
nn_unlock(&fs->ctx, fs->lock);
nn_setCError(computer, "no such file or directory");

View File

@ -6,8 +6,8 @@
typedef struct nn_vfnode {
struct nn_vfilesystem *fs;
struct nn_vfnode *parent;
char name[NN_MAX_PATH];
struct nn_vfnode *parent;
nn_bool_t isDirectory;
union {
// if directory
@ -57,8 +57,8 @@ nn_vfnode *nn_vf_allocFile(nn_vfilesystem *fs, const char *name) {
if(node == NULL) return NULL;
*node = (nn_vfnode) {
.fs = fs,
.parent = NULL,
.lastModified = nn_vf_now(fs),
.parent = NULL,
.isDirectory = false,
.data = NULL,
.len = 0,
@ -81,8 +81,8 @@ nn_vfnode *nn_vf_allocDirectory(nn_vfilesystem *fs, const char *name) {
}
*node = (nn_vfnode) {
.fs = fs,
.parent = NULL,
.lastModified = nn_vf_now(fs),
.parent = NULL,
.isDirectory = false,
.entries = buffer,
.len = 0,
@ -121,6 +121,44 @@ nn_size_t nn_vf_spaceUsedByNode(nn_vfnode *node) {
}
}
nn_vfnode *nn_vf_find(nn_vfnode *parent, const char *name) {
if(parent->isDirectory) return NULL;
for(nn_size_t i = 0; i < parent->len; i++) {
nn_vfnode *entry = parent->entries[i];
if(nn_strcmp(entry->name, name) == 0) {
return entry;
}
}
return NULL;
}
nn_bool_t nn_vf_ensureFileCapacity(nn_vfnode *file, nn_size_t capacity) {
if(file->isDirectory) return false;
nn_Alloc *alloc = &file->fs->ctx.allocator;
if(file->cap >= capacity) return true; // already at that point
char *newData = nn_resize(alloc, file->data, file->cap, capacity);
if(newData == NULL) {
return false; // OOM
}
file->data = newData;
file->cap = capacity;
return true;
}
// this is used to compute exponential backoff
// TODO: add an option to select either a slower growth rate or
// linear backoff to reduce memory usage at the cost of speed
nn_size_t nn_vf_getIdealCapacity(nn_vfnode *file, nn_size_t spaceNeeded) {
nn_size_t cap = file->cap;
if(cap == 0) cap = 1;
while(cap < spaceNeeded) cap *= 2; // this would mean a file with 1,048,577 bytes takes up 2,097,152 bytes, potentially wasting 1,048,575 bytes
return cap;
}
// methods
void nn_vfs_deinit(nn_vfilesystem *fs) {

View File

@ -408,7 +408,7 @@ sandbox = {
lower = string.lower,
wtrunc = function (str,space)
space = space - 1
return str:sub(1,(space >= utf8.len(str)) and (#str) or (utf8.offset(str,space+1)-1))
return unicode.sub(str, 1, space)
end,
}),
checkArg = checkArg,

View File

@ -388,7 +388,14 @@ nn_bool_t nn_path_isValid(const char *path) {
return nn_strlen(path) < NN_MAX_PATH; // less then because we depend on the terminator
}
nn_bool_t nn_path_canonical(const char path[NN_MAX_PATH], char canonical[NN_MAX_PATH]) {
static nn_bool_t nni_path_isAllDots(const char *path, nn_size_t len) {
for(nn_size_t i = 0; i < len; i++) {
if(path[i] != '.') return false;
}
return true;
}
nn_bool_t nn_path_canonical(const char *path, char canonical[NN_MAX_PATH]) {
// attempts to convert a random barely legal path
// if this shit is ever bugged and a sandbox escape is done
// by tricking it into sneaking some .. in there
@ -396,23 +403,49 @@ nn_bool_t nn_path_canonical(const char path[NN_MAX_PATH], char canonical[NN_MAX_
if(!nn_path_isValid(path)) {
// HELL NO
return false;
return true;
}
// tmp shit because lazy, maybe the optimizer be with us
char junk[NN_MAX_PATH];
// 0'd out because it fills it up with terminators, simplifying the rest of the code
// in theory this is suboptimal, however, I'm lazy
nn_memset(canonical, 0, NN_MAX_PATH);
size_t ptr = 0;
nn_size_t ptr = 0;
nn_size_t i = 0;
// TODO: burn it with fire and get banned from programming
for(nn_size_t i = 0; path[i]; i++) {
// all of this is very slow
while(true) {
while(path[i] == '/') i++; // just do not ask
if(path[i] == 0) break;
while(path[i] == '/') continue; // just do not ask
const char *subpath = path + i;
nn_size_t len = nn_path_firstSlash(subpath);
if(len == 0) {
len = nn_strlen(path) - i;
}
return true;
if(nni_path_isAllDots(subpath, len)) {
// we don't actually resolve them because they shouldn't be there
// to begin with
i += len;
continue;
}
if(ptr == 0) {
// at the start
nn_memcpy(canonical, subpath, len);
ptr = len;
i += len;
continue;
}
// just append to it
canonical[ptr] = '/';
ptr++;
nn_memcpy(canonical + ptr, subpath, len);
ptr += len;
i += len;
continue;
}
return false;
}