Update: I have changed the title to "Using Dispatch Tables To Improve Application Security" for clarity.
At a previous job, I saw some code that asked the user which function they wanted to run and then executed a subroutine with that name. This code demonstrates why such a practice is bad:
- use strict;
- use warnings;
- sub greet { print "Hello!\n" }
- sub inquire { print "How are you?\n" }
- sub bye { print "Farewell!\n" }
- sub delete_all_files { print "*KABOOM*\n" }
- sub insecure_call {
- no strict 'refs';
- shift->();
- }
- insecure_call('greet');
- insecure_call('inquire');
- insecure_call('bye');
- insecure_call('delete_all_files');
Output:
Hello!
How are you?
Farewell!
*KABOOM*
One solution to this is the dispatch table. With a dispatch table, you define up front which calls are legal for an outsider to make:
- use strict;
- use warnings;
- my %dispatch = (
- greet => \&greet,
- inquire => \&inquire,
- bye => \&bye,
- );
- sub greet { print "Hello!\n" }
- sub inquire { print "How are you?\n" }
- sub bye { print "Farewell!\n" }
- sub delete_all_files { print "*KABOOM*\n" }
- sub secure_call {
- my $call = shift;
- if ( ref $dispatch{$call} eq 'CODE' ) {
- $dispatch{$call}->();
- }
- else {
- warn "Invalid call $call";
- }
- }
- secure_call('greet');
- secure_call('inquire');
- secure_call('bye');
- secure_call('delete_all_files');
Output:
Hello!
How are you?
Farewell!
Invalid call delete_all_files at example_2a line 22.
The thing that bugs me about this particular solution (and I'll admit it's minor) is the repetition:
- my %dispatch = (
- greet => \&greet,
- inquire => \&inquire,
- bye => \&bye,
- );
To me, this reads like:
To go to greet, type 'greet'.
To go to inquire, type 'inquire'.
To go to bye, type 'bye'.
When it could just be asking "Which function do you wish to use?"
So, we could build the dispatch table dynamically from a list of acceptable calls:
- use strict;
- use warnings;
- my %dispatch;
- my @valid_calls = qw( greet inquire bye );
- sub greet { print "Hello!\n" }
- sub inquire { print "How are you?\n" }
- sub bye { print "Farewell!\n" }
- sub delete_all_files { print "*KABOOM*\n" }
- sub build_dispatch_table {
- no strict 'refs';
- %dispatch = map { $_ => *{$_} } @valid_calls;
- }
- sub secure_call {
- my $call = shift;
- if ( $dispatch{$call} ) {
- $dispatch{$call}->();
- }
- else {
- warn "Invalid call $call\n";
- }
- }
- build_dispatch_table();
- secure_call('greet');
- secure_call('inquire');
- secure_call('bye');
- secure_call('delete_all_files');
- print "\nBut, now this works because of the typeglob *{}\n";
- our @greet = qw( This is an array );
- print "@{$dispatch{greet}}\n";
- print "which annoys me even though it's probably inconsequential\n";
Output:
Hello!
How are you?
Farewell!
Invalid call delete_all_files
But, now this works because of the typeglob *{}
This is an array
which annoys me even though it's probably inconsequential
In addition to the typeglob annoyance, there is still a little repetition there: greet
, inquire
and bye
still appear more than once in the code. I don't actually find this to be a huge deal, but how might we solve those issues? One way is including the code itself in the dispatch table:
- use strict;
- use warnings;
- my %dispatch = (
- # Documentation for greet can go here
- greet =>
- sub {
- my $greeting = shift || 'Howdy!';
- print "$greeting\n";
- },
- # Documentation for inquire can go here
- inquire =>
- sub {
- print "How are you?\n";
- },
- # Documentation for farewell can go here
- farewell =>
- sub {
- print "Bye!\n";
- },
- );
- sub delete_all_files { print "*KABOOM*" }
- sub api {
- my $call = shift;
- if ( $dispatch{$call} ) {
- $dispatch{$call}->(@_);
- }
- else {
- warn "Not executing unknown API call $call\n";
- }
- }
- api('greet','Hello.');
- api('inquire');
- api('farewell');
- api('delete_all_files');
Output:
Hello.
How are you?
Bye!
Not executing unknown API call delete_all_files
One argument against this is it adds visual complexity to the code: it's one more layer that a new developer on the project would need to mentally parse before coming up-to-speed on the code. But, that may be minor, and I think these formatting choices are developer-friendly.
4 comments:
You could use attributes to register subs in to dispatch tables, something like
#!perl
use strict;use warnings;
use My::PackageBase;
sub actionone : Table('foo') {}
sub actiontwo : Table('foo') {}
sub actionttree : Table('bar') {}
launch->('foo','actiontwo', @myargs);
#!perl
use strict;use warnings;
package My::PackageBase;
# export stuff
my %dispatchtables;
sub launch {
my ($table,$action,@args) = @_;
die "Unknown $table:$action" unless $dispatchtables{$table}{$action};
$dispatchtables{$table}{$action}->(@args);
}
# lifted from Attribute::Handlers
my %symcache;
sub findsym {
my ($pkg, $ref, $type) = @_;
return $symcache{$pkg,$ref} if $symcache{$pkg,$ref};
$type ||= ref($ref);
no strict 'refs';
for my $sym ( values %{$pkg."::"} ) {
use strict;
next unless ref ( \$sym ) eq 'GLOB';
return $symcache{$pkg,$ref} = \$sym
if *{$sym}{$type} && *{$sym}{$type} == $ref;
}
}
my $builtin = qr/lvalue|method|locked|unique|shared/;
sub MODIFY_CODE_ATTRIBUTES {
my ($package, $subref, @attrs) = @_;
my $sym = findsym($package, $subref);
my $name = *{$sym}{NAME};
for my $k (@attrs) {
if ($k =~ m/^Table\((['"]?)(?<table>[A-z][A-z0-9_\-]+)\1\)$/) {
my $table = $+{table}
$dispatchtable{$table}{$name} = $subref
}
}
return grep { defined && !/$builtin/ } @attrs;
}
1;
# the above isnt tested, i just copy and pasted it from other things im working on then chopped it up a bit as an example
Hmm. I think you can fix the second example by using "can". So switching from
sub build_dispatch_table {
no strict 'refs';
%dispatch = map { $_ => *{$_} } @valid_calls;
}
to
sub build_dispatch_table {
%dispatch = map { $_ => __PACKAGE__->can($_) } @valid_calls;
}
Now "@{$dispatch{greet}}" will become invalid code.
Dispatch tables aren't bad. What you are showing is "coding something incorrectly can result in bad things." Dispatch tables are no exception to that.
In addition to using `can` to get a code ref you can just grab the CODE slot from the type globs:
%dispatch = map { $_ => *{$_}{CODE} } @valid_calls;
Post a Comment