Sakura Lang

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"),
  }
}
Edit on GitHub

On this page