-
-
Notifications
You must be signed in to change notification settings - Fork 443
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[4.x] Add DisallowSqliteAttach feature (#1283)
* queue.yml: remove TENANCY_VERSION env var from test.sh * add DisallowSqliteAttach feature * Fix code style (php-cs-fixer) * ci: add cd to each step * ci: simpler solution to race conditions, proper os/arch matrix * ci: fix runs-on matrix * ci: fix workflow on windows, fix makefile * Auto-build: Update extensions [skip ci] * Auto-build: Update extensions [skip ci] * ci: try fixing retry logic, make makefile use cl on Windows * ci: use the current branch for rebase * ci: try calling vcvars64 * ci: misc minor fixes * ci: try fixing c compiler on windows * ci: misc minor fixes * ci: add debug steps * ci: try to fix windows build * ci: try using clang on windows * ci: windows fixes, makefile fix * Auto-build: Update extensions [skip ci] * ci: dont produce .exp .lib on Windows * ci: try forcing shell: bash on commit step * ci: try to get linux cross-compilation working * ci: reformulate condition * ci: fix syntax error * ci: correct debian image name * Auto-build: Update extensions [skip ci] * ci: try to set up macOS cross-compilation * ci: add ARCH variable to makefile, override it during cross-compilation * Auto-build: Update extensions [skip ci] * ci: X64 -> x64 * ci: only trigger extensions.yml on pushes to extensions/ * fix tests on x64 * ci: try using bash for pushing on windows; ignore phpstan error * fix test failing in ci but passing locally * bump php version in composer.json, trigger extensions.yml build * remove comment * noattach: more explicit return values, avoid potential non-bool return values * makefile: use -Os on Windows * ci: use make -B * ci: try triggering extensions build on extensions.yml file changes * Auto-build: Update extensions [skip ci] * Auto-build: Update extensions [skip ci] * ci: remove windows linker flag, use a whitelist for git add * Auto-build: Update extensions [skip ci] * Auto-build: Update extensions [skip ci] * Auto-build: Update extensions [skip ci] * fix path in feature class, minor refactor * Fix code style (php-cs-fixer) --------- Co-authored-by: PHP CS Fixer <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
- Loading branch information
1 parent
6b38a35
commit a88a42f
Showing
18 changed files
with
356 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
name: Build extensions | ||
|
||
on: | ||
push: | ||
paths: | ||
- 'extensions/**' | ||
- '.github/workflows/extensions.yml' | ||
|
||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: true | ||
|
||
jobs: | ||
build: | ||
strategy: | ||
matrix: | ||
include: | ||
- os: ubuntu-latest | ||
arch: x64 | ||
- os: ubuntu-latest | ||
arch: ARM64 | ||
- os: windows-latest | ||
arch: x64 | ||
- os: macos-latest | ||
arch: x64 | ||
- os: macos-latest | ||
arch: ARM64 | ||
runs-on: ${{ matrix.os }} | ||
|
||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Download SQLite headers (Unix) | ||
if: runner.os != 'Windows' | ||
run: cd extensions && make headers | ||
|
||
- name: Download SQLite headers (Windows) | ||
if: runner.os == 'Windows' | ||
run: | | ||
cd extensions | ||
curl.exe -L https://www.sqlite.org/2024/sqlite-amalgamation-3470200.zip -o sqlite-src.zip | ||
Expand-Archive -Path sqlite-src.zip -DestinationPath . | ||
Copy-Item sqlite-amalgamation-3470200\sqlite3.h . | ||
Copy-Item sqlite-amalgamation-3470200\sqlite3ext.h . | ||
- name: Set up QEMU (Linux cross-compilation) | ||
if: runner.os == 'Linux' && matrix.arch == 'ARM64' | ||
uses: docker/setup-qemu-action@v3 | ||
|
||
- name: Build C files (Native Windows) | ||
if: runner.os == 'Windows' | ||
run: cd extensions && make -B | ||
|
||
- name: Build C files (Native Linux) | ||
if: runner.os == 'Linux' && matrix.arch == 'x64' | ||
run: cd extensions && make -B | ||
|
||
- name: Build C files (Linux cross-compilation) | ||
if: runner.os == 'Linux' && matrix.arch == 'ARM64' | ||
run: | | ||
cd extensions | ||
docker run --platform linux/arm64 \ | ||
-v .:/extensions \ | ||
debian:bookworm-slim \ | ||
bash -c "apt-get update && apt-get install -y make gcc && cd /extensions && make" | ||
- name: Build C files (Native macOS ARM64) | ||
if: matrix.os == 'macos-latest' && matrix.arch == 'ARM64' | ||
run: cd extensions && make -B | ||
|
||
- name: Build C files (macOS cross-compilation) | ||
if: matrix.os == 'macos-latest' && matrix.arch == 'x64' | ||
run: | | ||
cd extensions | ||
brew install llvm | ||
export CC=/opt/homebrew/opt/llvm/bin/clang | ||
export CFLAGS="-target x86_64-apple-darwin" | ||
export LDFLAGS="-target x86_64-apple-darwin" | ||
make -B ARCH=x86_64 | ||
- name: Commit output files | ||
shell: bash | ||
run: | | ||
cd extensions | ||
git config --local user.email "github-actions[bot]@users.noreply.github.com" | ||
git config --local user.name "github-actions[bot]" | ||
git add lib/*.{so,dylib,dll} lib/arm/*.{so,dylib} | ||
git commit -m "Auto-build: Update extensions [skip ci]" || echo "No changes to commit" | ||
- name: Push files | ||
shell: bash | ||
run: | | ||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) | ||
for attempt in {1..3}; do | ||
git pull --rebase origin $CURRENT_BRANCH && git push origin $CURRENT_BRANCH && exit 0 || { | ||
echo "Attempt $attempt failed. Retrying in 5 seconds..." | ||
sleep 5 | ||
} | ||
done | ||
echo "Failed to push changes after 3 attempts." | ||
exit 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
name: Validate code | ||
|
||
on: [push, pull_request] | ||
|
||
jobs: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
*.h | ||
*.zip | ||
sqlite-amalgamation-3470200/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
.PHONY: all headers | ||
|
||
OUTPUT := | ||
|
||
CCFLAGS += -shared -Os | ||
|
||
ifeq ($(OS),Windows_NT) | ||
OUTPUT = lib/noattach.dll | ||
CC = clang | ||
else | ||
UNAME := $(shell uname) | ||
CCFLAGS += -fPIC | ||
ARCH := $(if $(ARCH),$(ARCH),$(shell uname -m)) | ||
ifeq ($(UNAME),Darwin) | ||
ifeq ($(ARCH),arm64) | ||
OUTPUT = lib/arm/noattach.dylib | ||
else | ||
OUTPUT = lib/noattach.dylib | ||
endif | ||
else | ||
ifeq ($(ARCH),aarch64) | ||
OUTPUT = lib/arm/noattach.so | ||
else | ||
OUTPUT = lib/noattach.so | ||
endif | ||
endif | ||
endif | ||
|
||
$(info OUTPUT=$(OUTPUT)) | ||
|
||
all: $(OUTPUT) | ||
|
||
headers: | ||
# To simplify compilation across platforms, we include sqlite3ext.h in this directory. | ||
curl -L https://www.sqlite.org/2024/sqlite-amalgamation-3470200.zip -o sqlite-src.zip | ||
unzip sqlite-src.zip | ||
cp sqlite-amalgamation-3470200/*.h . | ||
|
||
$(OUTPUT): noattach.c | ||
# We don't link against libsqlite3 since PHP statically links its own libsqlite3. | ||
$(CC) $(CCFLAGS) -o $@ $< |
Empty file.
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
#include "sqlite3ext.h" | ||
SQLITE_EXTENSION_INIT1 | ||
|
||
static int deny_attach_authorizer(void *user_data, int action_code, const char *param1, const char *param2, const char *dbname, const char *trigger) { | ||
return action_code == SQLITE_ATTACH // 24 | ||
? SQLITE_DENY // 1 | ||
: SQLITE_OK; // 0 | ||
} | ||
|
||
#ifdef _WIN32 | ||
__declspec(dllexport) | ||
#endif | ||
int sqlite3_noattach_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) { | ||
SQLITE_EXTENSION_INIT2(pApi); | ||
|
||
if (sqlite3_set_authorizer(db, deny_attach_authorizer, 0) != SQLITE_OK) { | ||
*pzErrMsg = sqlite3_mprintf("Tenancy: Failed to set authorizer"); | ||
return SQLITE_ERROR; | ||
} else { | ||
return SQLITE_OK; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Stancl\Tenancy\Features; | ||
|
||
use Exception; | ||
use Illuminate\Database\Connectors\ConnectionFactory; | ||
use Illuminate\Database\SQLiteConnection; | ||
use Illuminate\Support\Facades\DB; | ||
use PDO; | ||
use Stancl\Tenancy\Contracts\Feature; | ||
use Stancl\Tenancy\Tenancy; | ||
|
||
class DisallowSqliteAttach implements Feature | ||
{ | ||
protected static bool|null $loadExtensionSupported = null; | ||
public static string|false|null $extensionPath = null; | ||
|
||
public function bootstrap(Tenancy $tenancy): void | ||
{ | ||
// Handle any already resolved connections | ||
foreach (DB::getConnections() as $connection) { | ||
if ($connection instanceof SQLiteConnection) { | ||
if (! $this->loadExtension($connection->getPdo())) { | ||
return; | ||
} | ||
} | ||
} | ||
|
||
// Apply the change to all sqlite connections resolved in the future | ||
DB::extend('sqlite', function ($config, $name) { | ||
$conn = app(ConnectionFactory::class)->make($config, $name); | ||
$this->loadExtension($conn->getPdo()); | ||
|
||
return $conn; | ||
}); | ||
} | ||
|
||
protected function loadExtension(PDO $pdo): bool | ||
{ | ||
if (static::$loadExtensionSupported === null) { | ||
static::$loadExtensionSupported = method_exists($pdo, 'loadExtension'); | ||
} | ||
|
||
if (static::$loadExtensionSupported === false) { | ||
return false; | ||
} | ||
if (static::$extensionPath === false) { | ||
return false; | ||
} | ||
|
||
$suffix = match (PHP_OS_FAMILY) { | ||
'Linux' => 'so', | ||
'Windows' => 'dll', | ||
'Darwin' => 'dylib', | ||
default => throw new Exception("The DisallowSqliteAttach feature doesn't support your operating system: " . PHP_OS_FAMILY), | ||
}; | ||
|
||
$arch = php_uname('m'); | ||
$arm = $arch === 'aarch64' || $arch === 'arm64'; | ||
|
||
static::$extensionPath ??= realpath(base_path('vendor/stancl/tenancy/extensions/lib/' . ($arm ? 'arm/' : '') . 'noattach.' . $suffix)); | ||
if (static::$extensionPath === false) { | ||
return false; | ||
} | ||
|
||
$pdo->loadExtension(static::$extensionPath); // @phpstan-ignore method.notFound | ||
|
||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
use Illuminate\Database\QueryException; | ||
use Illuminate\Support\Facades\DB; | ||
use Illuminate\Support\Facades\Event; | ||
use Illuminate\Support\Facades\Route; | ||
use Stancl\JobPipeline\JobPipeline; | ||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; | ||
use Stancl\Tenancy\Events\TenancyEnded; | ||
use Stancl\Tenancy\Events\TenancyInitialized; | ||
use Stancl\Tenancy\Events\TenantCreated; | ||
use Stancl\Tenancy\Features\DisallowSqliteAttach; | ||
use Stancl\Tenancy\Jobs\CreateDatabase; | ||
use Stancl\Tenancy\Jobs\MigrateDatabase; | ||
use Stancl\Tenancy\Listeners\BootstrapTenancy; | ||
use Stancl\Tenancy\Listeners\RevertToCentralContext; | ||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath; | ||
use Stancl\Tenancy\Tests\Etc\Tenant; | ||
|
||
test('sqlite ATTACH statements can be blocked', function (bool $disallow) { | ||
try { | ||
readlink(base_path('vendor')); | ||
} catch (\Throwable) { | ||
symlink(base_path('vendor'), '/var/www/html/vendor'); | ||
} | ||
|
||
if (php_uname('m') == 'aarch64') { | ||
// Escape testbench prison. Can't hardcode /var/www/html/extensions/... here | ||
// since GHA doesn't mount the filesystem on the container's workdir | ||
DisallowSqliteAttach::$extensionPath = realpath(base_path('../../../../extensions/lib/arm/noattach.so')); | ||
} else { | ||
DisallowSqliteAttach::$extensionPath = realpath(base_path('../../../../extensions/lib/noattach.so')); | ||
} | ||
|
||
if ($disallow) config(['tenancy.features' => [DisallowSqliteAttach::class]]); | ||
|
||
config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]); | ||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); | ||
Event::listen(TenancyEnded::class, RevertToCentralContext::class); | ||
|
||
Event::listen(TenantCreated::class, JobPipeline::make([ | ||
CreateDatabase::class, | ||
MigrateDatabase::class, | ||
])->send(function (TenantCreated $event) { | ||
return $event->tenant; | ||
})->toListener()); | ||
|
||
$tempdb1 = tempnam(sys_get_temp_dir(), 'tenancy_attach_test'); | ||
$tempdb2 = tempnam(sys_get_temp_dir(), 'tenancy_attach_test'); | ||
register_shutdown_function(fn () => @unlink($tempdb1)); | ||
register_shutdown_function(fn () => @unlink($tempdb2)); | ||
|
||
config(['database.connections.foo' => ['driver' => 'sqlite', 'database' => $tempdb1]]); | ||
config(['database.connections.bar' => ['driver' => 'sqlite', 'database' => $tempdb2]]); | ||
|
||
DB::connection('bar')->statement('CREATE TABLE secrets (key, value)'); | ||
DB::connection('bar')->statement('INSERT INTO secrets (key, value) VALUES ("secret_foo", "secret_bar")'); | ||
|
||
Route::post('/central-sqli', function () { | ||
DB::connection('foo')->select(request('q1')); | ||
return json_encode(DB::connection('foo')->select(request('q2'))); | ||
}); | ||
|
||
Route::middleware(InitializeTenancyByPath::class)->post('/{tenant}/tenant-sqli', function () { | ||
DB::select(request('q1')); | ||
return json_encode(DB::select(request('q2'))); | ||
}); | ||
|
||
tenancy(); // trigger features: todo@samuel remove after feature refactor | ||
|
||
if ($disallow) { | ||
expect(fn () => pest()->post('/central-sqli', [ | ||
'q1' => 'ATTACH DATABASE "' . $tempdb2 . '" as bar', | ||
'q2' => 'SELECT * from bar.secrets', | ||
])->json())->toThrow(QueryException::class, 'not authorized'); | ||
} else { | ||
expect(pest()->post('/central-sqli', [ | ||
'q1' => 'ATTACH DATABASE "' . $tempdb2 . '" as bar', | ||
'q2' => 'SELECT * from bar.secrets', | ||
])->json()[0])->toBe([ | ||
'key' => 'secret_foo', | ||
'value' => 'secret_bar', | ||
]); | ||
} | ||
|
||
$tenant = Tenant::create([ | ||
'tenancy_db_connection' => 'sqlite', | ||
]); | ||
|
||
if ($disallow) { | ||
expect(fn () => pest()->post($tenant->id . '/tenant-sqli', [ | ||
'q1' => 'ATTACH DATABASE "' . $tempdb2 . '" as baz', | ||
'q2' => 'SELECT * from bar.secrets', | ||
])->json())->toThrow(QueryException::class, 'not authorized'); | ||
} else { | ||
expect(pest()->post($tenant->id . '/tenant-sqli', [ | ||
'q1' => 'ATTACH DATABASE "' . $tempdb2 . '" as baz', | ||
'q2' => 'SELECT * from baz.secrets', | ||
])->json()[0])->toBe([ | ||
'key' => 'secret_foo', | ||
'value' => 'secret_bar', | ||
]); | ||
} | ||
})->with([true, false]); |