{"record":{"id":"de30062645c53a04","repo":"clockworklabs/SpacetimeDB","slug":"module-sql-views-cannot-read-a-module-restricted-table","errorCode":null,"errorMessage":"module SQL views cannot read a module-restricted table","messagePattern":"module SQL views cannot read a module-restricted table","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/core/src/host/wasm_common/module_host_actor.rs","lineNumber":200,"sourceCode":"    let auth = AuthCtx::for_current(database_identity);\n    let schema_view = SchemaViewer::new(&*tx, &auth);\n\n    // Compile to subscription plans.\n    let (plans, has_params) = SubscriptionPlan::compile(the_query, &schema_view, &auth)?;\n    ensure!(\n        !has_params,\n        \"parameterized SQL is not supported for view materialization yet\"\n    );\n\n    // Validate shape and disallow views-on-views.\n    for plan in &plans {\n        // This SQL originates in module code, not an authenticated external\n        // query. Check every source, including non-returned join inputs, before\n        // any plan executes. The checked env accessor is the only module path.\n        ensure!(\n            !plan\n                .table_ids()\n                .any(spacetimedb_datastore::system_tables::is_module_restricted_table),\n            \"module SQL views cannot read a module-restricted table\"\n        );\n        let Some(source_schema) = plan.return_table() else {\n            bail!(\"query does not return plain table rows\");\n        };\n        if plan.reads_from_view(true) || plan.reads_from_view(false) {\n            bail!(\"view definition cannot read from other views\");\n        }\n        if source_schema.row_type != *expected_row_type {\n            bail!(\n                \"query returns `{}` but view expects `{}`\",\n                fmt_algebraic_type(&AlgebraicType::Product(source_schema.row_type.clone())),\n                fmt_algebraic_type(&AlgebraicType::Product(expected_row_type.clone())),\n            );\n        }\n    }\n\n    let op = FuncCallType::View(call_info.clone());","sourceCodeStart":182,"sourceCodeEnd":218,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/eddf9f5014579a50d4b67630e28b6e15cad9c4af/crates/core/src/host/wasm_common/module_host_actor.rs#L182-L218","documentation":"SQL views executed from inside module code are compiled into a plan; `run_query_for_view` checks every table in the plan (including non-returned join inputs) against `is_module_restricted_table` before execution. Module-restricted tables are internal tables that module SQL must not read — only the checked env accessor path may reach them. This prevents module SQL views from bypassing access restrictions on internal/system tables.","triggerScenarios":"Executing a module SQL view (via `run_query_for_view`) whose plan reads any module-restricted table — directly in the returned rows, in a JOIN input, or through a subquery/view that touches such a table.","commonSituations":"Writing a SQL view inside a module that joins a regular table with an internal/restricted one; a view referencing another view that transitively reads a restricted table; refactors that silently widen a view to include system tables.","solutions":["Rewrite the module SQL/view so it does not reference any module-restricted table; use the checked environment accessor API instead for that data.","Inspect the view's plan dependencies: remove joins/subqueries that touch restricted tables even if they don't appear in the returned columns.","Restructure the query so restricted data is exposed only through the module's typed accessor (client-visible table API), then join the results in application code.","If the data must be queryable, expose it via a normal (non-restricted) table maintained by the module."],"exampleFix":"// before: view joins a module-restricted table\nlet plan = \"SELECT u.name, r.internal_data FROM users u JOIN restricted_tbl r ON u.id = r.id\";\n// after: read restricted data via the checked env accessor, join in module code\nlet restricted = env_accessor.restricted_lookup(id);\nlet plan = \"SELECT name FROM users WHERE id = ?\"; // then combine in code","handlingStrategy":"validation","validationCode":"// before adding a view, verify its sources\n// (module-side pseudo-check against plan table ids)\n// use spacetimedb_datastore::system_tables::is_module_restricted_table;\n// for table in view_source_tables() {\n//     assert!(!is_module_restricted_table(table), \"view reads restricted table\");\n// }","typeGuard":null,"tryCatchPattern":"match run_query_for_view(...) {\n    Ok(rows) => rows,\n    Err(e) if e.to_string().contains(\"module-restricted table\") => {\n        // fall back to the checked env accessor and rewrite the query\n        rewrite_view_without_restricted_tables();\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Never join internal/system tables into module SQL views","Trace transitive view dependencies — a view over another view can pull in restricted tables","Expose restricted data only via the checked environment accessor API"],"tags":["rust","sql","security","host"],"backgroundTag":"permission-denied","analyzedSha":"eddf9f5014579a50d4b67630e28b6e15cad9c4af","analyzedAt":"2026-09-20T12:15:59.611Z","contentChangedAt":"2026-09-20T12:15:59.611Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}