V2033 opaque_not_schema
A Schema bound (Json.decode, Sql.query rows, Llm.generate, Cli.parse, Tool.of arguments, a property parameter) reaches an opaque type outside its declaring module, directly or through a field (design §17.130).
An error from the type phase. sakura explain V2033 prints this explanation offline.
A Schema bound (Json.decode, Sql.query rows, Llm.generate, Cli.parse, Tool.of
arguments, a property parameter) reaches an opaque type outside its declaring module, directly
or through a field (design §17.130). Private construction comes in two kinds: an invariant that
is a predicate over the representation is a refinement type and decodes through its predicate
(Uuid, Path, Decimal); an invariant about how the value was produced (Query<R> from a
sql"…" literal, Html by escaping, UrlPart by encoding, File by the runtime) is opaque,
and data cannot build one. Decode the representation and call the module's constructor in the
open.
Wrong:
type Report = { title: String, body: Html }
let r: Report = Json.decode(text)?Fixed:
type ReportIn = { title: String, body: String }
let r: ReportIn = Json.decode(text)?
let body = Html.text(r.body)Wrong program
compiler/tests/ui/V2033_decode_opaque_html.sakura, which the compiler's ui suite runs through sakura check:
// The WS9.10 re-audit's second exploit: markup forged by decoding JSON into `Html`, then
// rendered through `html"…"`. `Html` is opaque outside `std/html`, so it is not `Schema` here.
import std/html.{Html}
import std/json.{DecodeError}
pub fn main() -> Unit ! {Console} {
let forged: Result<Html, DecodeError> = Json.decode("\{\"Html\":\"<script>evil()</script>\"\}")
match forged {
Ok(h) => Console.print(Html.to_string(html"<p>{h}</p>")),
Err(_) => Console.print("rejected"),
}
}sakura check --json reports this record (pretty-printed; $DIR is the fixture's directory):
{
"schema_version": "1.0",
"kind": "diagnostic",
"id": "d:0a7b90073701a01b",
"code": "V2033",
"severity": "error",
"phase": "type",
"message": "`Html` is opaque outside `std/html`, so it is not `Schema`: a value comes from its module's constructors, not from data (design §17.130)",
"snapshot": "sha256:$SNAPSHOT",
"symbol_id": "main::V2033_decode_opaque_html::main",
"spans": [
{
"uri": "file://$DIR/V2033_decode_opaque_html.sakura",
"content_hash": "sha256:a7a9afd859632742e97a2d058c182883ad7d476ce6c8569202ea2e954ccf8ec2",
"start_byte": 385,
"end_byte": 396,
"role": "primary"
}
],
"related_ids": [],
"expected": {
"render": "Schema"
},
"actual": {
"render": "Html"
},
"obligation": null,
"counterexample": null,
"explanation_uri": "sakura://explain/V2033",
"fixes": []
}More fixtures for this code: compiler/tests/ui/V2033_decode_opaque_query.sakura, compiler/tests/ui/V2033_decode_opaque_urlpart.sakura, compiler/tests/ui/V2033_property_opaque.sakura.
Fixed program
compiler/tests/ui/V2033_decode_opaque_html.fixed.sakura, which sakura check accepts with no diagnostics:
// Decode the representation (a `String`), then build the `Html` through its escaping
// constructor.
import std/html.{Html}
import std/json.{DecodeError}
pub fn main() -> Unit ! {Console} {
let decoded: Result<String, DecodeError> = Json.decode("\"<script>evil()</script>\"")
match decoded {
Ok(s) => Console.print(Html.to_string(html"<p>{Html.text(s)}</p>")),
Err(_) => Console.print("rejected"),
}
}