Ana içeriğe geç
友田 陽大
Application-layer security
Supabase
PostgreSQL
セキュリティ
TypeScript

Supabase / PostgreSQL のSQLインジェクション対策 — rpc・生SQL・関数内の動的SQLの罠

SupabaseはPostgRESTとパラメータ化クエリで素のSQLインジェクションが起きにくい。だが関数内の動的SQL(EXECUTEと文字列連結)・format()の%s誤用・生SQL・検索フィルタの組み立ては注入経路になる。EXECUTE USINGのパラメータ化、format()の%I/%L、quote_ident/literal、許可リスト検証で安全にする方法を、脆弱→修正の実SQL/TSで解説します。

Published
Reading time
20 min read
Author
友田 陽大
Paylaş

最初に結論を述べます。Supabase / PostgreSQL は「素のSQLインジェクション(SQLi)」が起きにくいスタックです。 PostgRESTが公開するREST APIも、supabase-js の構造化API(.eq().ilike() など)も、入力を文字列としてSQL文に埋め込むのではなく 値をパラメータとして渡す ため、入力がSQLの一部として解釈される余地がそもそも小さい。ですが——「起きにくい」は「起きない」ではありません。 安全な層の"外側"、すなわち 関数(RPC)の中の動的SQL・アプリ側の生SQL・検索フィルタ文字列の組み立て では、SQLiは今でも普通に起きます。

本記事は、Supabaseで「SQLiが起きにくい場所」と「それでも起きる場所」の地図を引き直し、攻撃がどう成立するか、そして パラメータ化・format()%I/%Lquote_ident/quote_literal・許可リスト検証 でどう塞ぐかを、脆弱→修正の実SQL/TSで示します。SQLインジェクションは OWASP Top 10 の Injection として長年トップクラスに位置づけられる古典ですが、Supabaseという組み合わせには特有の"再導入経路"があります。注入を含むアプリ層セキュリティ全体の地図はNext.js × Supabase アプリケーションセキュリティ完全ガイドにあり、本記事はその中の「SQLi」に絞って深掘りするものです。


1. なぜSupabaseでは「素のSQLi」が起きにくいのか

最初に、安全な側を正確に理解します。ここを誤解すると、安全な所を過剰に疑い、危険な所を見落とします。

SupabaseのデータアクセスはPostgREST経由が基本で、supabase-js の構造化APIはフィルタの をクエリ文字列のパラメータとして送ります。サーバー側のPostgRESTは、それを準備済みステートメントの バインド変数 としてPostgreSQLに渡します。つまり、ユーザー入力は最後まで「データ」であって「SQL文」にはなりません。

// 安全:値は最後までパラメータ。SQL文として解釈されない
const q = new URL(req.url).searchParams.get("q") ?? "";
const { data } = await supabase
  .from("posts")
  .select("title, body")
  .eq("status", "published")
  .ilike("title", `%${q}%`); // q は「値」。区切り文字を仕込んでもSQLは壊れない

q'; drop table posts; -- を入れても、それは「そういう文字列を含むタイトルを探す」だけで、SQL文の構造には一切影響しません。構造化APIを「値渡し」で使っている限り、SQLiは構造的に起きない。 これがSupabaseの初期設定が安全寄りである理由です。

問題は、開発者(やAIエージェント)が この安全な層の外に出る瞬間 に生まれます。次節で、その境界を地図にします。


2. では、どこで起きるのか——4つの危険地帯

SQLiが再導入されるのは、決まって以下の4箇所です。共通点は「入力がSQL文の"文字列"として組み立てられる」こと。値ではなく文になった瞬間に、注入の扉が開きます。

危険地帯何が起きるか主因
関数内の動的SQL(EXECUTE)RPCの中で入力を連結してSQL文を組む文字列連結 / %s
アプリ側の生SQLsql.unsafe や生クエリに入力を連結エスケープなしの連結
検索フィルタの組み立て.or()/.filter() のフィルタ文字列を連結PostgRESTフィルタ injection
任意SQL実行関数の公開exec_sql(text) のような汎用関数を晒す注入そのものをAPI化

このうち最も深刻で、最も見落とされやすいのが1つめ——関数(RPC)の中の動的SQL です。rpc() の呼び出し自体は安全に見えるため、危険が"関数の中"に隠れます。まずここを徹底的に分解します。


3. 動的SQLの本質——「EXECUTEで実行する文字列」だけが危ない

PL/pgSQLでは、SQL文を文字列として組み立ててから EXECUTE で実行できます(動的SQL)。SQLiの議論で最も重要なのは、「変数を値として使うこと」と「変数をSQL文の文字列に連結すること」はまったくの別物 だという一点です。ここが混同の温床です。

-- (A) 危険:入力を「SQL本文の文字列」に連結してから EXECUTE する
--     keyword は SQL の一部として解釈される=古典的SQLi
execute 'select * from documents where title = ''' || keyword || '''';

-- (B) 安全:通常の文(EXECUTE なし)。keyword は「値」として束縛される
--     ここでの || は文字列“値”の連結であって、SQL本文の組み立てではない
select * from documents where title = keyword;                   -- 完全一致
select * from documents where title ilike '%' || keyword || '%'; -- 部分一致

(B) の || を見て「連結しているから危険」と早合点しないでください。(B) では keyword は実行時に評価される テキスト値 にすぎず、PostgreSQLはそれをパラメータとして扱います。SQL文の構造は固定です。注入が起きるのは、組み立てた文字列を EXECUTE に渡したとき(A)だけ。 言い換えれば——

静的に書ける処理を、不要に EXECUTE で動的化しないこと。 これが第一の防御です。動的SQLは、テーブル名や列名を実行時に変える等、本当に必要なときだけの道具です。

動的SQLが本当に必要なときの安全な書き方は第6節にまとめます。その前に、Supabase特有の"見えにくさ"——rpc() を見ます。


4. rpc() の落とし穴——引数渡しは安全、関数の中身が危ない

SupabaseはPostgreSQLの関数を rpc() でAPI公開できます。ここで多くの人が誤解します。rpc() の引数受け渡し自体は安全です。PostgRESTが引数をパラメータとしてバインドするため、引数がSQL文に化けることはありません。危険なのは 関数の本体が、その引数で動的SQLを組み立てるとき です。

-- 脆弱:検索語を文字列連結で動的SQLに埋め込む RPC
create or replace function search_documents(keyword text)
returns setof documents
language plpgsql
security definer                       -- 所有者権限で実行=RLSを越える(後述)
as $$
begin
  return query execute
    'select * from documents where title ilike ''%' || keyword || '%''';
end;
$$;
// アプリ側:rpc の呼び出しはパラメータ化される(ここは安全)
const q = new URL(req.url).searchParams.get("q") ?? ""; // ← 汚染入力(source)
const { data } = await supabase.rpc("search_documents", { keyword: q });
// だが keyword は関数の中で SQL本文に連結される=注入は“関数の中”で起きる

攻撃は古典そのものです。q に次を入れます。

%' union select * from auth_tokens --

組み立てられるSQLはこうなります。

select * from documents where title ilike '%%' union select * from auth_tokens --%'

union で別テーブルを連結し、-- で末尾を無効化。documents に完璧なRLSが張ってあっても無関係 です。理由は次節。


5. SECURITY DEFINER が被害を最大化する

第4節の関数は security definer でした。これは関数を 定義者(所有者)の権限で実行する 設定で、典型的には所有者がテーブル所有者=RLSを越えられるロールです(PostgreSQL: CREATE FUNCTION)。つまり——

  • security invoker(既定)の関数内SQLi → 呼び出したユーザーの権限の範囲で被害が止まる
  • security definer の関数内SQLi → 所有者権限でRLSを丸ごと飛び越え、全テーブルを読み書きされる

Supabase公式も、service_role 同様にRLSを越える経路の存在を明記しています(Supabase: Row Level Security)。動的SQLと SECURITY DEFINER の組み合わせは、SQLiの被害を「テナント内の一部」から「データベース全体」へ拡大する増幅器 です。だからこの2つが重なる関数は、最優先で監査すべき対象になります。

SECURITY DEFINER 自体の正しい使い方(search_path の固定によるスキーマ乗っ取り対策、最小権限の所有者設計など)は論点が多いため、SECURITY DEFINER関数とsearch_pathの安全設計に切り出しています。本記事では「定義者権限の関数で動的SQLを組むなら、入力は絶対に文字列連結しない」という原則だけ徹底します。


6. 安全パターン——動的SQLが必要なときの4つの道具

ここからが対策の本体です。動的SQLが避けられないとき、入力の種類(値 / 識別子 / キーワード)ごとに正しい道具が違います。混ぜると穴が開きます。

6-1. 値は EXECUTE … USING のパラメータに分離する

最も安全で、最も使うべき手段です。$1 プレースホルダと USING 句で値を渡すと、入力は文字列連結を一切経由せず、バインド変数として扱われます。

-- 安全:値は $1 として USING で渡す(SQL本文には混ざらない)
execute 'select * from documents where title ilike $1'
  using '%' || keyword || '%'
  into result;
-- '%' || keyword || '%' は“値”の連結。$1 にバインドされるだけで構造は不変

6-2. 識別子は format('%I')——ただし本命は「許可リスト」

$1値にしか使えません。テーブル名・列名などの 識別子 はパラメータ化できないため、format()%I(内部で quote_ident を呼ぶ)で囲みます。%I は識別子として安全に引用符付けし、構文の破壊を防ぎます。

-- 識別子は %I、値は $1。両者を絶対に混ぜない
execute format('select count(*) from %I where owner_id = $1', target_table)
  using auth.uid()
  into result_count;

ここで 正直に線を引きます%Iquote_ident)が防ぐのは「識別子から抜け出して別のSQLを差し込む」という 構文上の注入 だけです。「その識別子を見せてよいか」という業務的な判断はしません。 攻撃者が target_table に実在する別テーブル(例:auth_secrets)を指定すれば、%I は何も警告せずそれを引用符付けして通します。だから識別子の本命の防御は 許可リスト検証 です。

create or replace function list_orders(sort_col text, dir text)
returns setof public.orders
language plpgsql
security invoker                      -- まず DEFINER が本当に要るか問い直す
set search_path = ''                  -- スキーマ乗っ取り対策(詳細は別記事)
as $$
begin
  -- 識別子(列名)は許可リストで検証する。%I だけでは「実在する別の列」を防げない
  if sort_col not in ('created_at', 'total_amount', 'status') then
    raise exception 'invalid sort column: %', sort_col;
  end if;
  -- 並び順は“キーワード”。値にも識別子にもできない=許可リスト必須
  if upper(dir) not in ('ASC', 'DESC') then
    raise exception 'invalid sort direction: %', dir;
  end if;

  return query execute
    format('select * from public.orders order by %I %s', sort_col, dir);
end;
$$;

dir%s(無検査の文字列置換)で埋めているのに安全なのは、直前で許可リスト検証を通したから です。ASC/DESC のようなSQLキーワードは値にも識別子にもできず、許可リスト→検証済みの値だけを置換、という順序でしか安全にできません。%s を使ってよいのは、このように「すでに自分で正当性を保証した値」だけです。

6-3. リテラルは %L(または quote_literal/quote_nullable)

値をどうしても文字列に埋め込む必要がある場面では、%L(内部で quote_literal)を使います。%L は文字列リテラルとして安全に引用符付けし、NULL も正しく NULL リテラルに変換します。

-- 安全:%L が keyword を安全なリテラルにエスケープする
execute format('select * from documents where title ilike %L', '%' || keyword || '%');

quote_literal を素で使うときの罠も押さえます。quote_literal(NULL) は文字列の 'NULL' ではなく SQLのNULL を返すため、連結すると壊れたSQLになります。NULLを取りうる値には quote_nullable を使うか、上記のように format%L に任せるのが安全です。

6-4. %s は「識別子・キーワードに対しては」使わない

format()%ssprintf 的な 無検査の文字列置換 です。識別子やキーワードに %s を使うと、%I の保護を失い、識別子注入を許します。

-- 脆弱:識別子に %s。quote_ident を経ないので注入できる
execute format('select * from %s', user_supplied_table);

-- 修正:識別子は %I(+許可リスト)
execute format('select * from %I', user_supplied_table);

4つの道具を一枚に整理します。

入力の種類正しい道具補足
値(VALUES, WHERE の比較値)$1USING最優先。連結を一切しない
リテラルとして埋め込む値%L / quote_literalNULLは %Lquote_nullable
識別子(テーブル名・列名)%I(+許可リスト)%I は構文のみ守る。妥当性は許可リスト
キーワード(ASC/DESC 等)許可リスト + 検証後に %s値にも識別子にもできない

7. アプリ側の生SQL——プレースホルダか、タグ付きテンプレートで

関数の外、アプリのサーバーコードから直接SQLを投げる場合(postgres.jsnode-postgres など)も、原則は同じ「連結しない」です。

import postgres from "postgres";
const sql = postgres(process.env.DATABASE_URL!);

const name = new URL(req.url).searchParams.get("name") ?? ""; // ← 汚染入力(source)

// 脆弱:sql.unsafe に文字列連結(生SQLのインジェクション)
const bad = await sql.unsafe(`select * from users where name = '${name}'`);

// 修正:タグ付きテンプレートが ${} を $1 プレースホルダに変換する(連結しない)
const good = await sql`select * from users where name = ${name}`;
// node-postgres:プレースホルダ+値配列。文字列連結は絶対にしない
await client.query("select * from users where id = $1 and org_id = $2", [id, orgId]);

ライブラリのタグ付きテンプレート(sql`...${value}...`)は、${value} を自動でバインド変数に変換するため安全です。危険なのは、その安全装置を 明示的に外す sql.unsafe(...) に入力を連結したときだけ。unsafe という名前は伊達ではありません。

そして、最悪の反パターンが「任意SQLを実行する汎用関数」の公開です。

-- 絶対NG:任意SQLを実行する汎用関数。rpc 経由で誰でも叩ける“注入のAPI化”
create function exec_sql(query text) returns void
language plpgsql security definer as $$
begin execute query; end;
$$;
-- supabase.rpc('exec_sql', { query: '...' }) で任意SQLが通る。存在自体が脆弱性

これは「注入の脆弱性」というより「注入をそのままAPIにした」もので、SECURITY DEFINER なら一撃でDB全体が陥落します。便利だからと作ってはいけません。


8. 検索フィルタの組み立て——.or() のPostgRESTフィルタ injection

最後の危険地帯は、supabase-js.or()/.filter()生のフィルタ文字列 を渡すケースです。

const q = new URL(req.url).searchParams.get("q") ?? "";

// 脆弱:検索語を .or() のフィルタ文字列に連結(PostgRESTフィルタ injection)
await supabase.from("posts").select("*").or(`title.ilike.*${q}*,body.ilike.*${q}*`);
// q に "," や "*"、")" を仕込むとフィルタ文法を壊し、別の条件を足して別の行を引ける

// 修正:構造化APIで「値」として渡す(フィルタ文字列を組み立てない)
await supabase.from("posts").select("title, body").ilike("title", `*${q}*`);

正直に区別します。 これは厳密にはPostgreSQLレベルの生SQLiではありません。PostgRESTは最終的なSQLをパラメータ化するため、drop table のようなSQL文を直接注入することはできない。しかし .or() のフィルタ 文法 を壊して条件を改ざんできるため、「どの行が返るか」を攻撃者が操作できます。RLSが弱いテーブルと組み合わさると、認可の前提が崩れます。論理的な注入として、れっきとした注入クラスの一員です。構造化API(.eq.ilike 等)に値を渡せば、フィルタ文字列を組み立てる必要自体がなくなります。


9. taint解析——汚染入力からSQLシンクへの到達を追う

ここまでの危険地帯は、見た目はバラバラですが構造は同じです。クライアントが操作できる入力(source)が、検証されないままSQL文を組み立てる処理(sink)に到達する。 だから正規表現ではなく、データフロー解析(taint解析)で機械的に追えます。

汚染入力(source)危険シンク(sink)なぜ危ないか
rpc の引数 / searchParams / リクエストボディ関数内の EXECUTE +文字列連結入力がSQL本文に混ざる(古典的SQLi)
同上format の %s(識別子・キーワード)無検査の置換=識別子注入
同上sql.unsafe / 生クエリの文字列連結アプリ層の生SQLインジェクション
同上.or() / .filter() のフィルタ文字列PostgRESTフィルタ injection(論理的注入)

source→sink の経路上に 妥当なサニタイザ$1/USING%I+許可リスト、%L、構造化API)があるかを見れば、危険な流れを自動で拾えます。SSRFやパストラバーサルも「source→sink」という同じ骨格で、入力をURLやパスのシンクに通すか否かで判定できます(SSRF対策ガイドでは同じ構造を別シンクで扱っています)。

ここで正直なスコープを引きます。 taint解析が機械化できるのは「汚染入力がサニタイザを経ずにSQLシンクへ到達したか」までです。サニタイザが"妥当か"——許可リストが正しい集合を網羅しているか、%I で囲った識別子の選択が業務的に妥当か——はツールには判定できません。 データフロー解析は関数内(intraprocedural)が基本で、関数が別の関数を呼ぶ流れや、文字列を組み立てて別の場所へ渡す流れは見逃します。検出は「よくある罠を踏んでいないこと」を示すだけで、「安全であること」を証明はしません。安全なクエリ設計は、最後は人間のレビューが要る——これが本記事の一貫した立場です。


10. 検出——npx で source→SQL sink を可視化する

設計で塞ぐと決めたら、「塞げているか」を継続的に検証します。私が公開しているOSS Aegis は、上のtaint解析(汚染入力→SQLシンク)に加え、supabase/migrations/**.sql を読んで 危険な動的SQLのパターンEXECUTE への文字列連結、format%s 識別子、SECURITY DEFINER +動的SQL、exec_sql 的な汎用関数)を洗い出します。インストール不要で走ります。

# インストール不要・設定不要。汚染入力→SQLシンクの到達と、
# migrations 内の危険な動的SQL(EXECUTE連結・format %s・DEFINER)を可視化
npx @aegiskit/cli scan

動的な確認も併用すると確度が上がります。SQLiの再現可否は、自分が所有する環境 に対して入力(区切り文字・union・コメント -- 等)を与え、エラーや想定外の行が返るかで確かめます。テスト観点はOWASPのテスティングガイドが体系的です(OWASP Web Security Testing Guide)。静的解析の「疑い」と動的の「再現」が一致したものを最優先で直す、という相関が効きます。


11. 本番前チェックリスト

外注でもAI製でも、本番投入の前に最低限これだけは確認してください。

  • 関数内の動的SQLで、値は EXECUTE … USING $1 に分離している(SQL本文へ連結しない)
  • 識別子は format%I で囲み、かつ 許可リストで検証している
  • リテラルは %L(または quote_literal/quote_nullable)。素の連結をしていない
  • format%s を識別子・キーワードに使っていない(使うなら許可リスト検証の後だけ)
  • 静的に書ける関数を、不要に EXECUTE で動的化していない
  • SECURITY DEFINER は本当に必要な関数だけ。search_path を固定している
  • アプリ側の生SQLは プレースホルダ($1)かタグ付きテンプレートsql.unsafe に入力を連結していない
  • 任意SQLを実行する汎用関数(exec_sql 等)を公開していない
  • 検索は .eq/.ilike など構造化APIで値渡し。.or() にユーザー入力を連結していない
  • 汚染入力→SQLシンクの静的解析をCIに常設している

発注者の視点で効く質問は3つです。「動的SQL(EXECUTE)を使っている関数はどれですか?」「SECURITY DEFINER の関数で入力をどう扱っていますか?」「検索はどう組み立てていますか?」——良い開発者は即答できます。


12. どこまで自分で、どこから監査か

最後に、正直に線を引きます。

汚染入力→SQLシンクの到達検出は、自動化で機械的に潰せます。 EXECUTE への連結、%s の識別子置換、sql.unsafe への連結、.or() への入力連結——これらは構造が定型的なので、静的解析をCIに入れれば人間が毎回見張る必要はありません。まずは Aegis(無料OSS、npx @aegiskit/cli scan)で現状を可視化するのが、最もコスパの良い第一歩です。

一方、「サニタイザが妥当か」「動的SQLの設計が正しいか」は人間の領域です。許可リストが正しい集合か、SECURITY DEFINER を使う必然性があるか、その関数がRLSを越えてよい範囲はどこまでか——これらは、あなたのデータモデルと業務ルールを理解した人間にしか判断できません。「このツールを入れればSQLiは完全に安全」と言い切る製品は、むしろ危険です。 ツールは検出を助けますが、クエリ設計の正しさは証明しません。

だからこそ線引きが要ります。動的SQLを含む関数の設計修正や、既存のSupabaseアプリのRPC・RLS・認可レビューが必要なら、セキュリティ監査で承ります。私自身、木材流通業界のDX案件で、221本のAPIとPostgreSQLの関数・RLSを含むデータ層の認可を実運用で設計・検証してきました。AIで速く作ること自体は正しい。速く作ったものを、漏らさず安全に固める——その仕組みづくりが本筋です。


よくある質問(FAQ)

Q. supabase-js の .eq().ilike() を使っていればSQLiは起きませんか? A. その経路では起きません。構造化APIに を渡している限り、入力はパラメータとして扱われ、SQL文の構造に影響しません。起きるのは、.or() にフィルタ文字列を連結する、RPCの中で動的SQLを組む、sql.unsafe を使う、といった「安全な層の外」に出たときだけです。

Q. rpc() を使うとSQLiの危険が増えますか? A. rpc() の呼び出し(引数渡し)自体は安全で、引数はパラメータとしてバインドされます。危険なのは 関数の本体 が、その引数を文字列連結で動的SQLに埋め込む場合です。危険は「rpc を使うこと」ではなく「関数の中身」にあります。

Q. format() を使えば安全ですか? A. プレースホルダ次第です。値は %L、識別子は %I なら安全側ですが、%s は無検査の文字列置換なので識別子・キーワードに使うと注入を許します。さらに %I は構文の破壊を防ぐだけで「その識別子を見せてよいか」は判定しません。識別子は %I許可リスト の二段で守ってください。

Q. RLSを張っていればSQLiは問題になりませんか? A. なりません。SECURITY DEFINER 関数の中のSQLiは RLSを丸ごと飛び越え ます。逆に SECURITY INVOKER でも、注入で意図しない行を引けば認可の前提は崩れます。RLSは必須ですが、SQLi対策(パラメータ化・許可リスト)は別レイヤーとして両方必要です。

Q. AIに「セキュアに書いて」と頼めば動的SQLは安全になりますか? A. 期待しすぎないでください。AIは「動くRPC」を最短で書きますが、EXECUTE への連結や %s の誤用はデモでは顕在化しません。検証ゲート(静的解析・テスト・レビュー)を通して初めて本番品質になります。


まとめ:安全な層の"外"に出る瞬間を見張る

要点を整理します。

  • Supabaseで「素のSQLi」が起きにくいのは、PostgRESTと構造化APIが 値をパラメータとして渡す から。だが 安全な層の外側——関数内の動的SQL・生SQL・検索フィルタの組み立て——では普通に起きる。
  • 最大の罠は SECURITY DEFINER 関数の中の動的SQL(EXECUTE +連結)。汚染入力がSQL本文に混ざると、所有者権限でRLSを越えて全テーブルを抜かれる。rpc() の引数渡し自体は安全でも、関数の中身が注入を再導入する。
  • 安全パターンは入力の種類で道具を分ける——値は $1/USING、リテラルは %L、識別子は %I +許可リスト、キーワードは許可リスト後に %s。そして 静的に書ける処理を不要に動的化しない
  • 汚染入力→SQLシンクの到達は taint解析で機械的に検出できる。だが サニタイザの妥当性とクエリ設計の正しさは、人間のレビューでしか担保できない。「入れれば完全に安全」な製品は存在しない。

既存のSupabaseアプリのRPC・動的SQL・RLSのレビューや、注入検出をCIに組み込む仕組みづくりが必要であれば、お気軽にご相談ください。


参考資料

友田

友田 陽大

Bakanlık (METI) Ödülü kazanan bir ürünün geliştiricisi. TypeScript + Python + AWS ile SaaS, endüstriyel dijital dönüşüm ve üretime hazır üretken yapay zekâyı (RAG) gereksinimlerden altyapıya ve işletmeye kadar tek başıma uçtan uca sunuyorum.

The measures in this article can be automated with a tool

Automate your Next.js / Supabase security controls with the OSS Aegis

Many of the measures here can be mechanically detected and hardened with a single middleware file and static analysis. With the free, MIT-licensed Aegis, you can scan your current project from one command. The vertical risks that need design, I also take on as an audit.

Hem proje bazlı işler hem de teknik danışmanlık için uygunum. 30 dakikalık ücretsiz bir görüşmeyle başlayın.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

Bunlar da ilginizi çekebilir