tilde.club CGI application with local storage
2025-04-03
It's been a few lives since pet played with CGI. Tilde club leaves no chance to avoid this forgotten craft.
Basically, there was no strong need to write this note. If an average pet was able find a solution in five minutes, others definitely can do better. But as long as CGI tutorial does exist, let this be an amendment.
Pet wanted simple log on the server written from client side with JavaScript. Thus, pet needed a storage in its home directory writable by CGI program. However, they say CGI scripts are running with NGNX credentials which means they cannot write to pet's home directory by default.
Home directory has group club
and NGINX is not in it.
Neither can pet set nginx
group for particular directory as an unprivileged user.
The only way to make a directory writable for CGI is to give
those permissions to everyone.
Pet believes tilde.club
is a friendly community, but minimal security is worth to apply.
It's not complicated, just two points.
The very basic thing is putting all publicly writable subdirectories under a directory for which read permissions are disabled, i.e.
chmod 701 /home/petbrain/publicly-private
Everyone can go through such directory but cannot list its content. Well-known subdirectories are still vulnerable, but if a subdirectory has long enough random name, it could be a perfect private storage.
That's all.
Finally, here's pet's first CGI program in its current life. It simply appends a record to file and return responses in JSON format.
Although responses contain neither quotes nor newlines,
pet assumes strerror
may return anything.
For this reason print_error
does the minimal escaping.
// for vasprintf:
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
char log_filename[] = "/home/petbrain/public_html/tw.myaw/test/visitors.myaw";
//char log_filename[] = "visitors.myaw";
extern char **environ;
char error_begin[] = "Status: 500\nContent-Type: application/json\n\n{\"status\": \"error\", \"description\": \"";
char error_end[] = "\"}\n";
void print_error(char* fmt, ...)
{
fputs(error_begin, stdout);
char* msg;
va_list ap;
va_start(ap);
int msg_len = vasprintf(&msg, fmt, ap);
va_end(ap);
if (msg_len == -1) {
fputs("Out of memory", stdout);
} else {
// escape double quotes, backslashes, and newlines for JSON output
for(int i = 0; i < msg_len; i++) {
char c = msg[i];
if (c == '"') {
putchar('\\');
putchar(c);
} else if (c == '\\') {
putchar('\\');
putchar('\\');
} else if (c == '\n') {
putchar('\\');
putchar('n');
} else {
putchar(c);
}
}
free(msg);
}
fputs(error_end, stdout);
}
int main(int argc, char* argv[])
{
FILE* log = fopen(log_filename, "a");
if (!log) {
print_error("Cannot open %s", log_filename);
return 0;
}
time_t t = time(NULL);
struct tm* tm = gmtime(&t);
if (tm == NULL) {
print_error("localtime: %s", strerror(errno));
return 0;
}
fprintf(log, "\n - ts::isodate: %04d-%02d-%02dT%02d:%02d:%02dZ\n",
tm->tm_year + 1900,
tm->tm_mon + 1,
tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
fprintf(log, " data:\n type: log\n content:\n");
for (char** env = environ;;) {
char* var = *env++;
if (var == nullptr) {
break;
}
fputs(" ", log); // indent
// print NAME=VALUE as NAME: VALUE
for (;;) {
char c = *var++;
if (c == 0) {
break;
}
if (c == '=') {
fputc(':', log);
fputc(' ', log);
} else {
fputc(c, log);
}
}
fputc('\n', log);
}
fclose(log);
puts("Status: 200\nContent-Type: application/json\n\n{\"status\": \"ok\"}\n");
return 0;
}