From c94fbb2b20c314bc0df40e14eaa5d053b306cb71 Mon Sep 17 00:00:00 2001
From: Rob Swindell <rob@synchro.net>
Date: Wed, 19 Jan 2022 19:21:14 -0800
Subject: [PATCH] Add JS methods for vetting filenames (e.g. for upload by
 users)

system.illegal_filename() - check if contains illegal chars/sequences
system.safest_filename() - check if contains only safest chars
system.allowed_filename() - check if meets criteria from SCFG->File Options
system.check_filename() - check if legal and meets configured criteria and is not in file.can
bbs.check_filename() - ditto, except will display badfile.msg as appropriate

Now scripts can utilize the sysop-controlled filename criteria and security of the BBS.
---
 src/sbbs3/js_bbs.cpp  |  34 +++++++++++
 src/sbbs3/js_system.c | 130 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 164 insertions(+)

diff --git a/src/sbbs3/js_bbs.cpp b/src/sbbs3/js_bbs.cpp
index 59d5b8eb05..a28b9ed1fa 100644
--- a/src/sbbs3/js_bbs.cpp
+++ b/src/sbbs3/js_bbs.cpp
@@ -1387,6 +1387,34 @@ js_user_event(JSContext *cx, uintN argc, jsval *arglist)
 	return(JS_TRUE);
 }
 
+static JSBool
+js_checkfname(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval *argv = JS_ARGV(cx, arglist);
+	sbbs_t*		sbbs;
+	char*		fname = NULL;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
+
+	if(argc < 1 || !JSVAL_IS_STRING(argv[0]))
+		return JS_TRUE;
+
+	if((sbbs = js_GetPrivate(cx, JS_THIS_OBJECT(cx, arglist))) == NULL)
+		return JS_FALSE;
+
+	JSVALUE_TO_MSTRING(cx, argv[0], fname, NULL);
+	if(fname == NULL)
+		return JS_FALSE;
+
+	rc=JS_SUSPENDREQUEST(cx);
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->checkfname(fname)));
+	JS_RESUMEREQUEST(cx, rc);
+	free(fname);
+
+	return JS_TRUE;
+}
+
 static JSBool
 js_chksyspass(JSContext *cx, uintN argc, jsval *arglist)
 {
@@ -4632,6 +4660,12 @@ static jsSyncMethodSpec js_bbs_functions[] = {
 	,316
 	},
 	/* security */
+	{"check_filename",	js_checkfname,		1,	JSTYPE_BOOLEAN,	JSDOCSTR("filename")
+	,JSDOCSTR("verify that the specified <i>filename</i> string is legal and allowed for upload "
+		"(based on system configuration), returns <i>true</i> if the filename is allowed.<br>"
+		"Note: Will display <tt>text/badfile.msg</tt> for matching filenames, if it exists.")
+	,31902
+	},
 	{"check_syspass",	js_chksyspass,		0,	JSTYPE_BOOLEAN,	JSDOCSTR("[sys_pw]")
 	,JSDOCSTR("verify system password, prompting for the password if not passed as an argument")
 	,310
diff --git a/src/sbbs3/js_system.c b/src/sbbs3/js_system.c
index d25b40ee52..f353f35152 100644
--- a/src/sbbs3/js_system.c
+++ b/src/sbbs3/js_system.c
@@ -1924,6 +1924,117 @@ js_chkname(JSContext *cx, uintN argc, jsval *arglist)
 
 	return(JS_TRUE);
 }
+
+static JSBool
+js_chkfname(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject *obj = JS_THIS_OBJECT(cx, arglist);
+	jsval *argv = JS_ARGV(cx, arglist);
+	char*		fname = NULL;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
+
+	if(argc < 1 || !JSVAL_IS_STRING(argv[0]))
+		return JS_TRUE;
+
+	js_system_private_t* sys;
+	if((sys = (js_system_private_t*)js_GetClassPrivate(cx,obj,&js_system_class))==NULL)
+		return JS_FALSE;
+
+	JSVALUE_TO_MSTRING(cx, argv[0], fname, NULL);
+	if(fname == NULL)
+		return JS_FALSE;
+
+	rc=JS_SUSPENDREQUEST(cx);
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(!illegal_filename(fname)
+		&& allowed_filename(sys->cfg, fname)
+		&& !trashcan(sys->cfg, fname, "file")));
+	JS_RESUMEREQUEST(cx, rc);
+	free(fname);
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_safest_fname(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject *obj = JS_THIS_OBJECT(cx, arglist);
+	jsval *argv = JS_ARGV(cx, arglist);
+	char*		fname = NULL;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
+
+	if(argc < 1 || !JSVAL_IS_STRING(argv[0]))
+		return JS_TRUE;
+
+	JSVALUE_TO_MSTRING(cx, argv[0], fname, NULL);
+	if(fname == NULL)
+		return JS_FALSE;
+
+	rc=JS_SUSPENDREQUEST(cx);
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(safest_filename(fname)));
+	JS_RESUMEREQUEST(cx, rc);
+	free(fname);
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_illegal_fname(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject *obj = JS_THIS_OBJECT(cx, arglist);
+	jsval *argv = JS_ARGV(cx, arglist);
+	char*		fname = NULL;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
+
+	if(argc < 1 || !JSVAL_IS_STRING(argv[0]))
+		return JS_TRUE;
+
+	JSVALUE_TO_MSTRING(cx, argv[0], fname, NULL);
+	if(fname == NULL)
+		return JS_FALSE;
+
+	rc=JS_SUSPENDREQUEST(cx);
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(illegal_filename(fname)));
+	JS_RESUMEREQUEST(cx, rc);
+	free(fname);
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_allowed_fname(JSContext *cx, uintN argc, jsval *arglist)
+{
+	JSObject *obj = JS_THIS_OBJECT(cx, arglist);
+	jsval *argv = JS_ARGV(cx, arglist);
+	char*		fname = NULL;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
+
+	if(argc < 1 || !JSVAL_IS_STRING(argv[0]))
+		return JS_TRUE;
+
+	js_system_private_t* sys;
+	if((sys = (js_system_private_t*)js_GetClassPrivate(cx,obj,&js_system_class))==NULL)
+		return JS_FALSE;
+
+	JSVALUE_TO_MSTRING(cx, argv[0], fname, NULL);
+	if(fname == NULL)
+		return JS_FALSE;
+
+	rc=JS_SUSPENDREQUEST(cx);
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(allowed_filename(sys->cfg, fname)));
+	JS_RESUMEREQUEST(cx, rc);
+	free(fname);
+
+	return JS_TRUE;
+}
+
 #endif
 
 static JSBool 
@@ -2128,6 +2239,25 @@ static jsSyncMethodSpec js_system_functions[] = {
 		"returns <i>true</i> if it is valid")
 	,315
 	},
+	{"check_filename",	js_chkfname,		1,	JSTYPE_BOOLEAN,	JSDOCSTR("filename")
+	,JSDOCSTR("verify that the specified <i>filename</i> string is legal and allowed for upload by users "
+		"(based on system configuration), returns <i>true</i> if the filename is allowed")
+	,31902
+	},
+	{"allowed_filename", js_allowed_fname,	1,	JSTYPE_BOOLEAN,	JSDOCSTR("filename")
+	,JSDOCSTR("verify that the specified <i>filename</i> string is allowed for upload by users "
+		"(based on system configuration), returns <i>true</i> if the filename is allowed")
+	,31902
+	},
+	{"safest_filename",	js_safest_fname,	1,	JSTYPE_BOOLEAN,	JSDOCSTR("filename")
+	,JSDOCSTR("verify that the specified <i>filename</i> string contains only the safest subset of characters")
+	,31902
+	},
+	{"illegal_filename", js_illegal_fname,	1,	JSTYPE_BOOLEAN,	JSDOCSTR("filename")
+	,JSDOCSTR("check if the specified <i>filename</i> string contains illegal characters or sequences, "
+		"returns <i>true</i> if it is an illegal filename")
+	,31902
+	},
 #endif
 	{"check_pid",		js_chkpid,			1,	JSTYPE_BOOLEAN,	JSDOCSTR("process-ID")
 	,JSDOCSTR("checks that the provided process ID is a valid executing process on the system, "
-- 
GitLab