Skip to content

Commit abf82ad

Browse files
Merge pull request #20 from Vectorial1024/security
Secure the task runners
2 parents a4bec08 + e1f32e8 commit abf82ad

File tree

6 files changed

+95
-5
lines changed

6 files changed

+95
-5
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,19 @@ Some tips:
116116
- If multiple tasks are started with the same task ID, then the task status object will only track the first task that was started
117117
- Known issue: on Windows, checking task statuses can be slow (about 0.5 - 1 seconds) due to underlying bottlenecks
118118

119+
### Securing the task runners
120+
The way this library works means that attackers (or other unwanted parties) may simply craft malicious commands that mimic legitimate usage of this library.
121+
122+
To secure the task runners from being started illegitimately, you may configure the `.env` file to contain the following key:
123+
124+
```
125+
PROCESS_ASYNC_SECRET_KEY=[your secret key here]
126+
```
127+
128+
You may need to clear your Laravel optimisation cache after changing this value.
129+
130+
The contents of the async tasks will be signed by this secret key, so that this library can know whether the tasks are started by this library itself or someone else.
131+
119132
## Testing
120133
PHPUnit via Composer script:
121134
```sh

src/AsyncTask.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
use loophp\phposinfo\OsInfo;
1414
use RuntimeException;
1515

16-
use function Opis\Closure\{serialize, unserialize};
16+
use function Opis\Closure\init;
17+
use function Opis\Closure\serialize;
18+
use function Opis\Closure\set_security_provider;
19+
use function Opis\Closure\unserialize;
1720

1821
/**
1922
* The common handler of an AsyncTask; this can be a closure (will be wrapped inside AsyncTask) or an interface instance.
@@ -147,6 +150,26 @@ public function __unserialize($data): void
147150
}
148151

149152
/**
153+
* Loads (or reloads) the secret key used by this library.
154+
*
155+
* Normally, this function does not need to be called by linrary users.
156+
* @return void
157+
*/
158+
public static function loadSecretKey(): void
159+
{
160+
// read from the env file for the secret key (if exists) to verify our identity
161+
$secretKey = env("PROCESS_ASYNC_SECRET_KEY");
162+
init(null);
163+
if ($secretKey != null && strlen($secretKey) > 0) {
164+
// we can set the secret key
165+
set_security_provider($secretKey);
166+
} else {
167+
// no secret key given, clear the security
168+
set_security_provider(null);
169+
}
170+
}
171+
172+
/*
150173
* Returns a status object for the started AsyncTask.
151174
*
152175
* If this task does not have an explicit task ID, a new one will be generated on-the-fly.

src/AsyncTaskRunnerCommand.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Vectorial1024\LaravelProcessAsync;
44

55
use Illuminate\Console\Command;
6+
use Opis\Closure\Security\SecurityException;
67

78
/**
89
* The Artisan command to run AsyncTask. DO NOT USE DIRECTLY!
@@ -23,6 +24,8 @@ class AsyncTaskRunnerCommand extends Command
2324
*/
2425
protected $description = 'Runs a background async task (DO NOT USE DIRECTLY)';
2526

27+
protected $hidden = true;
28+
2629
/**
2730
* Execute the console command.
2831
*
@@ -33,7 +36,13 @@ public function handle(): int
3336
// first, unpack the task
3437
// (Symfony already safeguards the "task" argument to make it required)
3538
$theTask = $this->argument('task');
36-
$theTask = AsyncTask::fromBase64Serial($theTask);
39+
try {
40+
$theTask = AsyncTask::fromBase64Serial($theTask);
41+
} catch (SecurityException $x) {
42+
// bad secret key; cannot verify sender identity
43+
$this->error("Unrecognized task giver is trying to start AsyncTaskRunner.");
44+
return self::FAILURE;
45+
}
3746
if ($theTask === null) {
3847
// bad underializing; probably bad data
3948
$this->error("Invalid task details!");

src/AsyncTaskStatus.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ private function findTaskRunnerProcess(): bool
128128
// we can assume we are in cmd, but wcim in cmd is deprecated, and the replacement gcim requires powershell
129129
$results = [];
130130
$fullCmd = "powershell echo \"\"(gcim Win32_Process -Filter \\\"CommandLine LIKE '%id=\'$encodedTaskID\'%'\\\").ProcessId\"\"";
131-
\Illuminate\Support\Facades\Log::info($fullCmd);
132131
exec("powershell echo \"\"(gcim Win32_Process -Filter \\\"CommandLine LIKE '%id=\'$encodedTaskID\'%'\\\").ProcessId\"\"", $results);
133132
// will output many lines, each line being a PID
134133
foreach ($results as $candidatePID) {

src/ProcessAsyncServiceProvider.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ class ProcessAsyncServiceProvider extends ServiceProvider
1111
*/
1212
public function register(): void
1313
{
14-
// pass
14+
// load/reset our secret key
15+
AsyncTask::loadSecretKey();
1516
}
1617

1718
/**

tests/AsyncTaskTest.php

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use InvalidArgumentException;
66
use LogicException;
7+
use Opis\Closure\Security\SecurityException;
78
use RuntimeException;
89
use Vectorial1024\LaravelProcessAsync\AsyncTask;
910
use Vectorial1024\LaravelProcessAsync\AsyncTaskStatus;
@@ -13,10 +14,18 @@
1314
use Vectorial1024\LaravelProcessAsync\Tests\Tasks\TestTimeoutErrorTask;
1415
use Vectorial1024\LaravelProcessAsync\Tests\Tasks\TestTimeoutNoOpTask;
1516
use Vectorial1024\LaravelProcessAsync\Tests\Tasks\TestTimeoutNormalTask;
17+
use function Opis\Closure\init;
18+
use function Opis\Closure\set_security_provider;
1619

1720
class AsyncTaskTest extends BaseTestCase
1821
{
19-
// directly running the runner
22+
public function setUp(): void
23+
{
24+
// we need to reset the security key in case some of our tests change the secret key
25+
AsyncTask::loadSecretKey();
26+
}
27+
28+
// directly running the runner
2029

2130
public function testCanRunClosure()
2231
{
@@ -74,6 +83,42 @@ public function testConfigNoTimeLimit()
7483
$this->assertNull($task->getTimeLimit());
7584
}
7685

86+
public function testCanVerifySender()
87+
{
88+
// test that, when given a serialized closure signed by us, we can correctly reproduce the closure
89+
$testSecretKey = "LaravelProcAsync";
90+
$testClosure = fn () => "Hello there!";
91+
// reset the security provider
92+
set_security_provider(null);
93+
init($testSecretKey);
94+
// serialize once
95+
$testTask = new AsyncTask($testClosure);
96+
$checkingString = $testTask->toBase64Serial();
97+
// then unserialize it
98+
$reproducedTask = AsyncTask::fromBase64Serial($checkingString);
99+
$this->assertTrue($reproducedTask instanceof AsyncTask);
100+
}
101+
102+
public function testCannotVerifySender()
103+
{
104+
// test that, when given a serialized closure signed by Mallory, we can correctly reject the closure
105+
$testSecretKey = "LaravelProcAsync";
106+
$testMalloryKey = "HereComesDatMallory";
107+
$testClosure = fn () => "Hello there!";
108+
// reset the security provider
109+
// mallory appears!
110+
set_security_provider($testMalloryKey);
111+
// serialize once
112+
$testTask = new AsyncTask($testClosure);
113+
$checkingString = $testTask->toBase64Serial();
114+
// then unserialize it
115+
set_security_provider($testSecretKey);
116+
$this->expectException(SecurityException::class);
117+
// should throw
118+
$reproducedTask = AsyncTask::fromBase64Serial($checkingString);
119+
unset($reproducedTask);
120+
}
121+
77122
// ---------
78123

79124
// integration test with the cli artisan via a mocked artisan file, which tests various features of this library

0 commit comments

Comments
 (0)