Skip to content

Instantly share code, notes, and snippets.

@ilyakurdyukov
Last active January 19, 2022 04:32
Show Gist options
  • Save ilyakurdyukov/db5e81b87a7be5ac38813104c111799f to your computer and use it in GitHub Desktop.
Save ilyakurdyukov/db5e81b87a7be5ac38813104c111799f to your computer and use it in GitHub Desktop.
Helper function for escaping special characters from user-provided strings to be used in calls to system().
#ifndef SAFESYS_H
#define SAFESYS_H
#include <stdlib.h>
#include <stdarg.h>
#ifndef SAFESYS_NOMEM
#include <errno.h>
#define SAFESYS_NOMEM ENOMEM
#endif
static int safesys(const char *format, ...) {
va_list va; size_t size = 0;
const char *next, *p, *f, *esc;
char *d, *cmd, c; int ret, nquot;
esc = "\t\n\r !\"#$&()*;<=>?[\\]^`{|}~";
va_start(va, format);
f = format;
while ((c = *f++)) if (c == '%') {
p = next = va_arg(va, const char*);
nquot = 0;
while ((c = *p++))
nquot = c == '\'' ?
size += nquot > 1 ? 3 : nquot + 1, 0 :
nquot + !!strchr(esc, c);
size += (nquot > 1 ? 2 : nquot) + (p - next) - 2;
}
va_end(va);
size += f - format;
cmd = malloc(size);
if (!cmd) return SAFESYS_NOMEM;
#define SAFESYS_COPY \
if (nquot > 1) *d++ = '\''; \
while (next < p - 1) { \
c = *next++; \
if (nquot == 1 && strchr(esc, c)) \
*d++ = '\\', nquot = 0; \
*d++ = c; \
} \
if (nquot > 1) *d++ = '\'';
d = cmd;
va_start(va, format);
f = format;
while ((c = *f++))
if (c != '%') *d++ = c;
else {
p = next = va_arg(va, const char*);
nquot = 0;
while ((c = *p++))
if (c == '\'') {
SAFESYS_COPY
nquot = 0; next++;
*d++ = '\\'; *d++ = '\'';
} else nquot += !!strchr(esc, c);
SAFESYS_COPY
}
va_end(va);
*d = 0;
puts(cmd);
ret = system(cmd);
free(cmd);
return ret;
}
#endif // SAFESYS_H
@lcn2
Copy link

lcn2 commented Jan 18, 2022

This code is very worthy of consideration.

A suggestion: Focus the code to only forming a "safe" string to make it more general.

You might wish to have the code simply return a malloced string that is "safe" and let the caller pass it to system() or popen() or even write to to a file for some other purpose. Then let the caller determine when they want to free the malloc-ed string the your code builds.

@lcn2
Copy link

lcn2 commented Jan 18, 2022

Consider the hypothetical need to execute the following command:

cd ~/tmp && wc -l -- __unsafe_arg__

Here the "unsafe_arg" needs to be escape protected, but the other args are perfectly fine.

Also consider the hypothetical need to execute this command:

ls -ld -- __user_arg__ .. > foo 2> /dev/null

Here the "user_arg .." is a variable number of arguments that need to be escape protected, but the other args are perfectly fine.

etc.

You might consider more modal interface that allows some arguments to be protected while other arguments pass thru unprotected.

@lcn2
Copy link

lcn2 commented Jan 18, 2022

While you might consider a more modal interface, you may wish to offer different types of argument protection:

  • back quoting shell unsafe chars in a given arg
  • single routes around an given arg
  • double quotes around a given arg (say if the arg is something like $FOO)

@lcn2
Copy link

lcn2 commented Jan 18, 2022

Minor suggestion:

You might wish to split the code into a safesys.c and a safesys.h to make is easier to link with and to make is less awkward other code to use in more than one .c file.

Again this code is an excellent concept that is, I believe, worthy of refinement.

@ilyakurdyukov
Copy link
Author

@lcn2
It's written as a concept because you'll rewrite it anyway.
But there is a working code for escaping single quotes that suits the needs of mkiocccentry.

cd ~/tmp && wc -l -- __unsafe_arg__ is done like this:

safesys("cd ~/tmp && wc -l -- %", __unsafe_arg__);

@ilyakurdyukov
Copy link
Author

ilyakurdyukov commented Jan 19, 2022

mkiocccentry does not require double quotes escaping, because if one wants to use $FOO on the command line to invoke mkiocccentry, those variables will be expanded by the shell before mkiocccentry is run.

@ilyakurdyukov
Copy link
Author

You might wish to have the code simply return a malloced string that is "safe" and let the caller pass it to system() or popen()

This can be done very simply:

-static int safesys(const char *format, ...) {
+static char* cmdprintf(const char *format, ...) {
 
-	if (!cmd) return SAFESYS_NOMEM;
+	if (!cmd) return cmd;
 
-	puts(cmd);
-	ret = system(cmd);
-	free(cmd);
-	return ret;
+	return cmd;

I just want to keep system() in this gist as an example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment