Ralph Ritoch
Ralph Ritoch

Reputation: 3440

How do you convert a relative path to an absolute path in C?

I am trying to make a suid application that will only execute ruby scripts located in a restricted folder. I have tried to do this using realpath(3) but it is only returning the first segment of the path. Below is my code...

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>

#define SUEXEC_STR_LEN 2048
#define RUBY_APP "/usr/bin/ruby"
#define DIRECTORY_SEPARATOR "/"

static void safepath(const char *path_in, char * path_out, int outlen) {
    realpath(path_in, path_out);
}

int main ( int argc, char *argv[] )
{
    char cmd[SUEXEC_STR_LEN];
    char path_out[SUEXEC_STR_LEN];
    char path_in[SUEXEC_STR_LEN];

    char *cp = &cmd[0];

    strncpy(cp, RUBY_APP, SUEXEC_STR_LEN - 1);

    strncpy(path_in, DIRECTORY_SEPARATOR, SUEXEC_STR_LEN - 1);
    strncat(path_in,argv[1],SUEXEC_STR_LEN - 1);

    safepath(path_in,path_out,SUEXEC_STR_LEN - 1);

    printf("path_in=%s path_out=%s\n",path_in,path_out);

    setuid( 0 );
    // system( cmd );

    return 0;
}

This is an example of the result I'm getting

root@server01:/root/src# ./a.out foo/bar/../test
path_in=/foo/bar/../test path_out=/foo

This is the result I want

root@server01:/root/src# ./a.out foo/bar/../test
path_in=/foo/bar/../test path_out=/foo/test

Upvotes: 3

Views: 2732

Answers (3)

Ralph Ritoch
Ralph Ritoch

Reputation: 3440

This is the code which I used as a solution to the problem. It may have some bugs remaining in it, and it isn't checking the outlen argument to avoid segfaults and other uglyness but it seems to get the job done.

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <linux/limits.h>

#define SUEXEC_STR_LEN 2048
#define RUBY_APP "/usr/bin/ruby"
#define DIRECTORY_SEPARATOR "/"
#define RUBY_EXT ".rb"

#define SERVICES_BASE_PATH "/path/to/ruby/services"

static inline int isDirSeparator(const char c) { return (c == '/' || c == '\\'); }

static void safepath(const char *path_in, char * path_out, int outlen)
{
    char *dirs[PATH_MAX];
    int depth = 0;
    char *dstptr = path_out;
    const char *srcptr = path_in;

    *dstptr++ = DIRECTORY_SEPARATOR[0];
    dirs[0] = dstptr;
    dirs[1] = NULL;
    depth++;

    while (1) {
        if ((srcptr[0] == '.') && isDirSeparator(srcptr[1])) {
            srcptr += 2;
        } else if (srcptr[0] == '.' && srcptr[1] == '.' && isDirSeparator(srcptr[2])) {
            if (depth > 1) {
                dirs[depth] = NULL;
                depth--;
                dstptr = dirs[depth-1];
            } else {
                dstptr = dirs[0];
            }
            srcptr += 3;
        } else if (srcptr[0] == '.' && srcptr[1] == '.' && srcptr[2] == 0) {
            if (depth == 1) {
                srcptr += 2;
            } else {
                depth--;
                dstptr = dirs[depth-1];
                srcptr += 2;
            }
        } else {
            while (!isDirSeparator(srcptr[0]) && srcptr[0]) {
                *dstptr++ = *srcptr++;
            }
            if (srcptr[0] == 0) {
                if (dstptr != dirs[0] && isDirSeparator(dstptr[-1])) {
                    dstptr[-1] = 0;
                }
                dstptr[0] = 0;
                return;
            } else if (isDirSeparator(srcptr[0])) {
                if (dstptr == dirs[0]) {
                    srcptr++;
                } else {
                    *dstptr++ = *srcptr++;
                    dirs[depth] = dstptr;
                    depth++;
                }
                while (isDirSeparator(srcptr[0]) && srcptr[0]) {
                    srcptr++;
                }
            } else {
                path_out[0] = 0;
                return;
            }
        }
    }
}

int main ( int argc, char *argv[] )
{
    int ret;
    char cmd[SUEXEC_STR_LEN];
    char path_out[SUEXEC_STR_LEN];
    char path_in[SUEXEC_STR_LEN];

    char *cp = &cmd[0];


    if (argc < 2) {
        fprintf(stderr,"usage: %s <service>\n",argv[0]);
        return 1;
    }
    strncpy(cp, RUBY_APP, SUEXEC_STR_LEN - 1);

    strncpy(path_in, DIRECTORY_SEPARATOR, SUEXEC_STR_LEN - 1);
    strncat(path_in,argv[1],SUEXEC_STR_LEN - 1);

    safepath(path_in,path_out,SUEXEC_STR_LEN - 1);

    //printf("path_in=%s path_out=%s\n",path_in,path_out);

    strncat(cmd," ",SUEXEC_STR_LEN - (1+sizeof(RUBY_EXT)));

    strncat(cmd,SERVICES_BASE_PATH,SUEXEC_STR_LEN - (1+sizeof(RUBY_EXT)));
    strncat(cmd,path_out,SUEXEC_STR_LEN - (1+sizeof(RUBY_EXT)));
    strncat(cmd,RUBY_EXT,SUEXEC_STR_LEN - 1);

    setuid( 0 );
    ret = system( cmd );
    if (ret == -1) {
        return ret;
    }
    ret =  WEXITSTATUS(ret);
    return ret;
}

Upvotes: 0

Emmet
Emmet

Reputation: 6401

So, here's a working sketch of how you might go about it in C on Linux. This is a quick hack that I do not represent as being exemplary code, efficient, etc. It (ab)uses PATH_MAX, uses “bad” string functions, and may leak memory, eat your cat, and have corner cases that segfault, etc. When it breaks, you get to keep both parts.

The basic idea is to go through the given path, breaking it up into “words” using “/” as the delimiter. Then, go through the list, pushing the “words” onto a stack, but ignoring if empty or “.”, and popping if “..”, then serializing the stack by starting at the bottom and accumulating a string with slashes in between.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <linux/limits.h>

typedef struct stack_s {
    char *data[PATH_MAX];
    int   top;
} stack_s;

void stack_push(stack_s *s, char *c) {
    s->data[s->top++] = c;
}

char *stack_pop(stack_s *s) {
    if( s->top <= 0 ) {
        return NULL;
    }
    s->top--;
    return s->data[s->top];
}

// DANGER! DANGER! Returns malloc()ed pointer that you must free()
char *stack_serialize(stack_s *s) {
    int i;
    char *buf;
    int len=1;

    for(i=0; i<s->top; i++) {
        len += strlen(s->data[i]);
        len++; // For a slash
    }
    buf = malloc(len);
    *buf = '\0';
    for(i=0; i<s->top-1; i++) {
        strcat(buf, s->data[i]);
        strcat(buf, "/");
    }
    strcat(buf, s->data[i]);
    return buf;
}

// DANGER! DANGER! Returns malloc()ed pointer that you must free()
char *semicanonicalize(char *src) {
    char *word[PATH_MAX] = {NULL};
    int   w=0;
    int   n_words;

    char *buf;
    int   len;
    char *p, *q;

    stack_s dir_stack = {{NULL},0};

    // Make a copy of the input string:
    len = strlen(src);
    buf = strdup(src);

    // Replace slashes with NULs and record the start of each "word"
    q = buf+len;
    word[0]=buf;
    for(p=buf,w=0; p<q; p++) {
        if(*p=='/') {
            *p = '\0';
            word[++w] = p+1;
        }
    }
    n_words=w+1;

    // We push w[0] unconditionally to preserve slashes and dots at the
    // start of the source path:
    stack_push(&dir_stack, word[0]);

    for(w=1; w<n_words; w++) {
        len = strlen(word[w]);
        if( len == 0 ) {
            // Must've hit a double slash
            continue;
        }
        if( *word[w] == '.' ) {
            if( len == 1 ) {
                // Must've hit a dot
                continue;
            }
            if( len == 2 && *(word[w]+1)=='.' ) {
                // Must've hit a '..'
                (void)stack_pop(&dir_stack);
                continue;
            }
        }
        // If we get to here, the current "word" isn't "", ".", or "..", so
        // we push it on the stack:
        stack_push(&dir_stack, word[w]);
    }

    p = stack_serialize(&dir_stack);
    free(buf);
    return p;
}


int main(void)
{
    char *in[] = { "/home/emmet/../foo//./bar/quux/../.",
                   "../home/emmet/../foo//./bar/quux/../.",
                   "./home/emmet/../foo//./bar/quux/../.",
                   "home/emmet/../foo//./bar/quux/../."
    };
    char *out;
    for(int i=0; i<4; i++) {
        out = semicanonicalize(in[i]);
        printf("%s \t->\t %s\n", in[i], out);
        free(out);
    }
    return 0;
}

Upvotes: 1

timrau
timrau

Reputation: 23058

You should check for realpath()'s return value. As described in its man page,

RETURN VALUE
If there is no error, realpath() returns a pointer to the resolved_path.

Otherwise it returns a NULL pointer, and the contents of the array resolved_path are undefined. The global variable errno is set to indicate the error.

Also in ERRORS section of its man page,

ENOENT The named file does not exist.

Thus, if there is indeed no /foo/test in your file system, realpath() should return NULL and the output is undefined.

Upvotes: 2

Related Questions