Beefy Boxes and Bandwidth Generously Provided by pair Networks
"be consistent"
 
PerlMonks  

Easy Script Editor

by George_Sherston (Vicar)
on Oct 05, 2001 at 17:31 UTC ( #116988=sourcecode: print w/replies, xml ) Need Help??
Category: CGI
Author/Contact Info George_Sherston
Description: I wrote this script so that I can edit my scripts directly on my remote web space, rather than going through all the business of uploading, changing the shebang line from what I need to run locally to what I need to run in the web space etc etc. It also backs up the script each time I edit it.

I'm posting this as much in the hope of getting some comments on overall programming style as to offer something that others might find useful (though it wd be great if somebody did find it useful).

I'd be particularly grateful for comments on the security aspects of this script, as I have only a very limited grasp of the issues, and am not at all sure it's secure enough to go on my web space.
#!/usr/bin/perl -w

use strict;
use Cwd;
use CGI qw/:standard/;
use DBI;
use File::Find;

my $ext = 'pl';
my @dirs = (getcwd);
my $q = new CGI;

&CheckPass;

sub
CheckPass
{

# check the password and user name, 
# send log in page if they are wrong or absent
# go to &Main of they are ok

    if ($q->param('UserName') and $q->param('PassWord')) {
        my $dbh = DBI->connect("DBI:mysql:database=mydb");
        my $ref = $dbh->selectcol_arrayref("SELECT * FROM " . $q->para
+m('UserName') . " WHERE PassWord = " . $q->param('PassWord'));
        $dbh->disconnect;
        if ($ref->[0]) {
            &Main;
        }
        else {
            &LogInPage('Log In Failed',$q->param('Action') ? $q->param
+('Action') : 'FileTree',$q->param('File'));
        }
    }
    else {
        &LogInPage('Hello There',$q->param('Action') ? $q->param('Acti
+on') : 'FileTree',$q->param('File'));
    }    
}
    
sub
Main
{

# look at the 'Action' parameter, and depending
# on what it is, send the file tree page, send the script editor,
# send the script editor with lines wrapped / unwrapped,
# or save the file

    if ($q->param('Action') eq "FileTree") {
        my $cookie = $q->cookie(
            -name=>'editor',
            -value=>$q->param('UserName'),
            -expires=>'+10y',
        );
        print $q->header(        
            -cookie=>$cookie               # set the cookie in case th
+is is the first visit
        );
        print $q->start_html;
        find(\&GetFileTree,@dirs);
        print $q->end_html;
    }
    elsif ($q->param('Action') eq "GetScript") {
        (my $Table = $q->param('File')) =~ s/\//_/g;
        $Table =~ s/\.pl//;
        my $dbh = DBI->connect("DBI:mysql:database=hudex");
        my $sth = $dbh->do("DESCRIBE $Table");
        unless ($sth) {                   # create the back up db tabl
+e unless it already exists
            $dbh->do("CREATE TABLE $Table (ID MEDIUMINT UNSIGNED PRIMA
+RY KEY NOT NULL AUTO_INCREMENT, Script LONGTEXT, Date BIGINT UNSIGNED
+)");
        }
        open FILE, $q->param('File') or die "can't open file $!";
        read FILE, my $buffer, -s(FILE);
        $buffer =~ s/'/\\'/g;
        $dbh->do("INSERT INTO $Table VALUES (NULL, '$buffer', " . time
+ . ")");    # back up the script
        &EditPage($q->param('File'));
    }
    elsif ($q->param('Action') eq "Wrap") {
        my $Wrap = $q->param('WrapStatus') eq 'OFF' ? 'PHYSICAL' : 'OF
+F';
        &EditPage($q->param('File'),$Wrap,'ReSend');
    }
    elsif ($q->param('Action') eq "Save") {
        my @Script = split /\n/, $q->param('Script');
        my $File = $q->param('File');
        open FILE, ">$File" or die "can't open $File $!";
        my %subhash = (               # respect to japhy's excellent r
+egex book for this move
            '&lt;' => '<',
            '&gt;' => '>',
        );
        for (@Script) {
            chomp;
            $_ = substr($_, 5, (length $_) - 5) if /\A\d/;
            s/(&gt;|&lt;)/$subhash{$1}/g;                      # resto
+re html tags for saving
            print FILE $_,"\n";
        }
        close FILE;
        &EditPage($q->param('File'));
    }
    else {
        &LogInPage('Hello There',$q->param('Action') ? $q->param('Acti
+on') : 'FileTree',$q->param('File'));
    }    
}


sub
EditPage
{

# send the script to a browser window in the form of a <textarea> fiel
+d
# optional arguments: $Wrap toggles line wrapping on and off; 
# $Send tells us whether to send back the contents of $q->param('File'
+)
# unchanged or retrieve them from $File

    my ($File, $Wrap, $Send) = @_;
    $Wrap = $Wrap ? $Wrap : 'OFF';
    (my $Title = $File) =~ s/(.{1})/$1 /g;
    $Title = 'E D I T :: ' . $Title;
    print $q->header;
    print $q->start_html(
        -title=>$Title,
    );
    unless ($Send eq 'ReSend') {
        open FILE, "$File" or die "couldn't open $File, $!";
        print "WARNING: FILE MAY BE TOO LARGE TO BACK UP!<BR>" if -s(F
+ILE) > 2e32;    # warn if file is larger than BIGTEXT (which is most 
+unlikely)
    }
    print $q->start_form(
        -action=>'editor.pl',
        -method=>'POST',
        -name=>'mainform',
    );
    print $q->submit(
        -name=>'Action',
        -value=>'Wrap',
    );
    print $q->submit(
        -name=>'Action',
        -value=>'Save',
    );
    $q->param(
        -name=>'File',
        -value=>$File
    );
    print $q->hidden('File');
    print $q->hidden('UserName');
    print $q->hidden('PassWord');
    $q->param(
        -name=>'WrapStatus',
        -value=>$Wrap
    );
    print $q->hidden('WrapStatus');
    print "<textarea name=\"Script\" wrap=\"$Wrap\" style=\"overflow:a
+uto;width:100%;height:95%\">";
    my %subhash = (               # respect to japhy's excellent regex
+ book for this move
        '<' => '&lt;',
        '>' => '&gt;',
    );
    if ($Send eq 'ReSend') {
        (my $Script = $q->param('Script') )=~ s/(<|>)/$subhash{$1}/g;;
        print $Script;
    }
    else {
        my $counter = 1;
        for (<FILE>) {
            s/(<|>)/$subhash{$1}/g;                  # make sure there
+ aren't any html tags inside the textarea
            print $counter, (" " x (4 - int(log($counter)/log(10)))), 
+$_ ;
            $counter++;
            $counter = $counter < 10000 ? $counter : 1;
        }
    }
    print '</textarea>';
    print $q->end_form();
    print $q->end_table();
    print $q->end_html;
}


sub
LogInPage
{

# send the log in page.  Arguments: 
# $Header sends a friendly or informative message to the user; 
# $Action tells the script what to do after the log in is successful;
# $File tells the script what file to do it with

    my $UserName = $q->param('UserName') ? $q->param('UserName') : $q-
+>cookie('editor');
    my ($Header,$Action,$File) = @_;
    print $q->header;
    print $q->start_html('E D I T O R :: L O G :: I N ');
    print '<table width="100%" height="100%"><tr valign=middle><td ali
+gn=center>';
    print $q->start_form(
        -action=>'editor.pl',
        -method=>'POST',
    );
    print "<h3>$Header:</h3>";
    print 'User Name ';
    print $q->textfield(
        -name=>'UserName',
        -value=>$UserName,
    );
    print '<BR>Password ';
    print $q->textfield('PassWord');
    print '<BR>';
    print $q->submit(
        -value=>'Log In',
    );
    $q->param(
        -name=>'Action',
        -value=>$Action,
    );
    print $q->hidden('Action');
    $q->param(
        -name=>'File',
        -value=>$File,
    );
    print $q->hidden('File');
    print $q->end_form();
    print '</td></tr></table>';
    print $q->end_table();
    print $q->end_html;
}

sub 
GetFileTree
{
    print "<div style=\"margin-left:$Posn\"><a href=\"editor.pl?Action
+=GetScript&File=$File::Find::name\" target=\"$File::Find::name\">$Fil
+e::Find::name</a><\div>" if /\.$ext\Z/;
}
Replies are listed 'Best First'.
Re: Easy Script Editor
by pixel (Scribe) on Oct 05, 2001 at 19:17 UTC

    I agree completely with ajt's worries about the security of what you're doing, but I'd also like to raise a couple of points about your use of CGI.pm.

    CGI.pm has two APIs - an object oriented interface and a functional interface. Although the majority of references seem to use the OO interface, I think that the functional interface is simpler to use.

    In the functional interface you import a set of functions into your program's namespace. You do this by passing optional arguments into the use CGI statement like this:

    use CGI qw(:standard);

    :standard defines a particular set of functions that will cover most of your CGI needs. Having imported the functions you can just use them without having to create an object. Like this:

    use CGI qw(:standard); my $name = param('name'); print header, start_html, h1('Test Page'); print p("The name is $name"); print end_html;

    On the other hand, if you use the OO interface you need to create a CGI object and then access all of the functions thru that object. Like this:

    use CGI; my $q = CGI-new; my $name = $q->param('name'); print $q->header, $q->start_html, $q->h1('Test Page'); print $q->p("The name is $name"); print $q->end_html;

    It's not that much more typing, but I think that the functional version looks neater.

    I mention this, because you use the functional way of loading CGI.pm but then go on to use the OO interface thoughout your script. This shows that you may be slightly confused.

    Oh, and one more thing about the OO interface. Using syntax like:

    my $q = new CGI;

    instead of

    my $q = CGI->new;

    Is going to work fine 999 times out of a 1000, but it can occasionally lead to very hard to track down bugs. Read what Damian Conway says about the indirect object syntax in Object Oriented Perl for a full description of the problems.

    Blessed Be
    The Pixel

      Thanks very much for that. You're right, I have been jolly confused about the differences between the two ways of using CGI.pm, and, as you can see, got around that confusion by ignoring it. Not a good long term strategy. But I think I do now understand it rather better, and I'm most grateful.

      I must say I don't understand OO programming. And I feel I ought to, and I ought to use it for preference because it's... more advanced, grown up or something. This leads me into using it in ways I don't really understand what I'm doing - not a good thing. Perhaps I shouldn't get hung up on that, and just go with the functional approach which makes more intuitive sense to me.

      George Sherston

        OO is good when you have lots of instances of the same class, each carrying around their own pieces of data. But in a CGI script, it's very rare that you'd have more than one CGI object - therefore I think that the OO interface is unnecessary.

        (Actually, with the functional interface, it is creating a CGI object, but you don't need to know about it.)

        Blessed Be
        The Pixel

Re: Easy Script Editor
by ajt (Prior) on Oct 05, 2001 at 18:16 UTC
    First off you're not running in taint mode, which you must do whenever you let outside users, write to the local file system. Read perlsec to see how it works.

    Even if you have taing mode enabled, passwords are easy to guess and crack, and I fear that you're essentially giving someone permssion to write any script they feel like in your CGI-BIN, and then run it my pointing a browser at it. This is a very bad thing...

    I would strongly consider if you really need this? and unless it's a burning desparte need, I wouldn't do it.

    If you have to be able to do this, then you must make sure your passwords can't be broken easily, you enable taint mode, and you run your CGI-BIN in some sand box environment, and you are hope you need to be lucky...

    Be paranoid, very paranoid....

Re: Easy Script Editor
by jj808 (Hermit) on Oct 05, 2001 at 19:01 UTC
    I fully agree with ajt about the password issue. Hopefully you will have configured your database to reject any connections from the outside world, otherwise you would have a *big* security hole as the passwords do not appear to be stored in an encrypted form.

    A technique which I have used in the past is to have your CGI program send an email to the administrator whenever an invalid password is entered. Make sure the email contains the username, password, time, and originating IP address. The 'Login failed' page can then include this information, details of the Computer Misuse Act, and notification that the hacking attempt has been recorded. Obviously this won't stop a determined hacker, but might scare the script kiddies and at least you'll know if someone's trying to break in when your mailbox starts filling up...

    Cheers,

    JJ

Re: Easy Script Editor
by mandog (Curate) on Oct 06, 2001 at 09:20 UTC
    I notice that you do a
    SELECT *
    ovid's node Death to Select Star! suggests that this may not be a good idea.

    You may consider using placeholders btrott explains why you might want to user them

    To my ignorant eye, it looks like you have a seperate table for each userID. This feels weird and looks like it might make it hard to do stuff like:

    my $sql="HERE_DOC; SELECT p.userID, LEN(p.passwd) FROM password p WHERE LEN(p.passwd)<6 GROUP BY p.userID; HERE_DOC"
    ....Also to my eye, HERE documents are nicer way to format SQL than a single line of text.

    It might be easier to follow if your main function wasn't CheckPass(). It is a bit more conventional to do something like this inside main():

    if ( !CheckPass() ){ #bail }else{ # continue }
    You might define some global constants and comments at the top of your program so if the name of your database changes, you or your replacement could change the constant without really remembering how your script worked.
    use constant SCRIPT_DB => 'script_db' # use constant PASSWORD_TABLE => 'passwd'; #
    As others have pointed out, you may wish to enable taint mode by putting a -T as an argument to perl on your #! line (oddly enough perldoc perlrun indicates this will workin in windoze as well

    This will make your program choke on this line:

    open FILE, $q->param('File')
    ...because you have not removed all the shell characters and somebody could ask you to open a | pipe to a bad command or two

    ..Hope this helps. I'm sure a more knowledgeable monk will point out any problems with my problems



    email: mandog

Update: Re: Ongoing chmod lack of success
by George_Sherston (Vicar) on Oct 06, 2001 at 21:20 UTC
    NB the sharp-eyed will notice that this doesn't make any sense here - I put it in the WRONG THREAD and it is awaiting re-parenting by a friendly editor</strong

    Just to say, for the benefit of posterity,

    (A) Thanks again to all the brothers and sisters who helped out with this;

    (B) For the next episode, in which I put some of this into practice but still get stuck, see this node.

    George Sherston

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: sourcecode [id://116988]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others imbibing at the Monastery: (4)
As of 2021-12-09 09:57 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    R or B?



    Results (36 votes). Check out past polls.

    Notices?