Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4.x] Add DisallowSqliteAttach feature #1283

Merged
merged 49 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
613ab5b
queue.yml: remove TENANCY_VERSION env var from test.sh
stancl Dec 31, 2024
9bb06af
add DisallowSqliteAttach feature
stancl Jan 2, 2025
ededbbe
Fix code style (php-cs-fixer)
Jan 2, 2025
59e9590
ci: add cd to each step
stancl Jan 2, 2025
cf0a8ca
ci: simpler solution to race conditions, proper os/arch matrix
stancl Jan 2, 2025
3866f59
ci: fix runs-on matrix
stancl Jan 2, 2025
f032b45
ci: fix workflow on windows, fix makefile
stancl Jan 2, 2025
68a7795
Auto-build: Update extensions [skip ci]
github-actions[bot] Jan 2, 2025
93fe982
Auto-build: Update extensions [skip ci]
github-actions[bot] Jan 2, 2025
c5f187d
ci: try fixing retry logic, make makefile use cl on Windows
stancl Jan 2, 2025
a784ee0
ci: use the current branch for rebase
stancl Jan 2, 2025
a5b2bbc
ci: try calling vcvars64
stancl Jan 2, 2025
688a4df
ci: misc minor fixes
stancl Jan 2, 2025
8b8ae26
ci: try fixing c compiler on windows
stancl Jan 2, 2025
b9c09ef
ci: misc minor fixes
stancl Jan 2, 2025
594fa92
ci: add debug steps
stancl Jan 2, 2025
3368a6d
ci: try to fix windows build
stancl Jan 2, 2025
2bee323
ci: try using clang on windows
stancl Jan 2, 2025
772cbd9
ci: windows fixes, makefile fix
stancl Jan 2, 2025
cef9d7e
Auto-build: Update extensions [skip ci]
github-actions[bot] Jan 2, 2025
b0861e1
ci: dont produce .exp .lib on Windows
stancl Jan 2, 2025
aca2359
ci: try forcing shell: bash on commit step
stancl Jan 2, 2025
88855ec
ci: try to get linux cross-compilation working
stancl Jan 2, 2025
a3e58ee
ci: reformulate condition
stancl Jan 2, 2025
c320138
ci: fix syntax error
stancl Jan 2, 2025
2c7f1fc
ci: correct debian image name
stancl Jan 2, 2025
04ff761
Auto-build: Update extensions [skip ci]
github-actions[bot] Jan 2, 2025
611195e
ci: try to set up macOS cross-compilation
stancl Jan 2, 2025
bc12957
ci: add ARCH variable to makefile, override it during cross-compilation
stancl Jan 2, 2025
2d33402
Auto-build: Update extensions [skip ci]
github-actions[bot] Jan 2, 2025
3eb7868
ci: X64 -> x64
stancl Jan 2, 2025
1bb8a42
ci: only trigger extensions.yml on pushes to extensions/
stancl Jan 2, 2025
693f05a
fix tests on x64
stancl Jan 2, 2025
73ab190
ci: try using bash for pushing on windows; ignore phpstan error
stancl Jan 2, 2025
3b3d580
fix test failing in ci but passing locally
stancl Jan 2, 2025
7524328
bump php version in composer.json, trigger extensions.yml build
stancl Jan 2, 2025
2440e43
remove comment
stancl Jan 2, 2025
381e71b
noattach: more explicit return values, avoid potential non-bool retur…
stancl Jan 4, 2025
69067ed
makefile: use -Os on Windows
stancl Jan 4, 2025
3ece4a0
ci: use make -B
stancl Jan 4, 2025
ef44bff
ci: try triggering extensions build on extensions.yml file changes
stancl Jan 4, 2025
5573db2
Auto-build: Update extensions [skip ci]
github-actions[bot] Jan 4, 2025
e7292c6
Auto-build: Update extensions [skip ci]
github-actions[bot] Jan 4, 2025
03552e1
ci: remove windows linker flag, use a whitelist for git add
stancl Jan 4, 2025
7e19a41
Auto-build: Update extensions [skip ci]
github-actions[bot] Jan 4, 2025
60d7718
Auto-build: Update extensions [skip ci]
github-actions[bot] Jan 4, 2025
9c4d02d
Auto-build: Update extensions [skip ci]
github-actions[bot] Jan 4, 2025
e2e20ad
fix path in feature class, minor refactor
stancl Jan 4, 2025
143b494
Fix code style (php-cs-fixer)
Jan 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions .github/workflows/extensions.yml
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
2 changes: 1 addition & 1 deletion .github/workflows/queue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ jobs:
run: |
cd tenancy-queue-tester
TENANCY_VERSION=${VERSION_PREFIX}#${GITHUB_SHA} ./setup.sh
TENANCY_VERSION=${VERSION_PREFIX}#${GITHUB_SHA} ./test.sh
./test.sh
2 changes: 2 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
name: Validate code

on: [push, pull_request]

jobs:
Expand Down
4 changes: 4 additions & 0 deletions INTERNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@
The `ci.yml` workflow includes support for [act](https://github.com/nektos/act).

To run all tests using act, run `composer act`. To run only certain tests using act, use `composer act-input "FILTER='some test name'"` or `composer act -- --input "FILTER='some test name'"`.

Helpful note: GHA doesn't mount the project at /var/www/html like the docker compose setup does. This can be observed in act where the inner container's filesystem structure will match the host.

Also, for debugging act you can just add a job that does `sleep 1h` and then `docker ps` + `docker exec -it <id> bash`.
1 change: 1 addition & 0 deletions assets/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@
// Stancl\Tenancy\Features\TenantConfig::class,
// Stancl\Tenancy\Features\CrossDomainRedirect::class,
// Stancl\Tenancy\Features\ViteBundler::class,
// Stancl\Tenancy\Features\DisallowSqliteAttach::class,
],

/**
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@
"docker-up": "docker compose up -d",
"docker-down": "docker compose down",
"docker-restart": "docker compose down && docker compose up -d",
"docker-rebuild": "PHP_VERSION=8.3 docker compose up -d --no-deps --build",
"docker-rebuild": "PHP_VERSION=8.4 docker compose up -d --no-deps --build",
"docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml",
"testbench-unlink": "rm ./vendor/orchestra/testbench-core/laravel/vendor",
"testbench-link": "ln -s vendor ./vendor/orchestra/testbench-core/laravel/vendor",
"testbench-link": "ln -s /var/www/html/vendor ./vendor/orchestra/testbench-core/laravel/vendor",
"testbench-repair": "mkdir -p ./vendor/orchestra/testbench-core/laravel/storage/framework/sessions && mkdir -p ./vendor/orchestra/testbench-core/laravel/storage/framework/views && mkdir -p ./vendor/orchestra/testbench-core/laravel/storage/framework/cache",
"coverage": "open coverage/phpunit/html/index.html",
"phpstan": "vendor/bin/phpstan --memory-limit=256M",
Expand Down
3 changes: 3 additions & 0 deletions extensions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.h
*.zip
sqlite-amalgamation-3470200/
41 changes: 41 additions & 0 deletions extensions/Makefile
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 added extensions/lib/.gitkeep
Empty file.
Empty file added extensions/lib/arm/.gitkeep
Empty file.
Binary file added extensions/lib/arm/noattach.dylib
Binary file not shown.
Binary file added extensions/lib/arm/noattach.so
Binary file not shown.
Binary file added extensions/lib/noattach.dll
Binary file not shown.
Binary file added extensions/lib/noattach.dylib
Binary file not shown.
Binary file added extensions/lib/noattach.so
Binary file not shown.
22 changes: 22 additions & 0 deletions extensions/noattach.c
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;
}
}
72 changes: 72 additions & 0 deletions src/Features/DisallowSqliteAttach.php
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;
}
}
106 changes: 106 additions & 0 deletions tests/Features/NoAttachTest.php
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]);