Reading and writing JSON
Build a JSON response in a web service — and understand why escaping user data is not optional.
JSON is in the standard library, so you avoid a dependency for something as common as responding to an API call.
bruk std.web som web
bruk std.json som json
funksjon api_status(ctx: ordbok_tekst) -> ordbok_tekst {
web.route("GET /api/status")
la svar = "{" + json.streng("status") + ":" + json.streng("ok") + "}"
returner web.response_builder(200, {"content-type": "application/json"}, svar)
}
funksjon start() -> heltall {
skriv("API-et er klart")
returner 0
}What happens here
The content type application/json is not decoration. It tells the client how to interpret the response, and many client libraries refuse to parse a response marked as plain text. Set the wrong type, and you get debugging rounds that are about everything other than the actual error.
Notice that the key also goes through json.streng. It seems excessive when the key is a fixed text you wrote yourself, but the habit is worth having: the day the key comes from a variable, the code is already correct.
Escaping is not optional
This is the most important part of the recipe. If a value from the user is to go into JSON, it must be escaped.
// WRONG: the name may contain quotation marks and break out of the string
la farlig = "{" + json.streng("navn") + ":" + navn + "}"
// RIGHT: let the library escape the value
la trygg = "{" + json.streng("navn") + ":" + json.streng(navn) + "}"Think through what happens in the first variant if someone registers with a name that contains a quotation mark. The string ends in the middle, and the rest of the name is interpreted as JSON structure. At best you get an invalid response. At worst, whoever wrote the name decides what the rest of the document looks like — and if the recipient trusts the fields, you have given away control.
What makes this error dangerous is that it does not show up in testing. All the names you try yourself work fine. The error only appears the day real user data hits the code.
Same rule, three places
The rule is the same for HTML, SQL and JSON: never build structured text by pasting user data together. Use the escape function for the format you are writing to.
| Format | Function | What it protects against |
|---|---|---|
| HTML | nf.html_escape | input being run as script in the browser |
| SQL | nf.sql_text | input changing what the query does |
| JSON | json.streng | input breaking out and changing the document structure |
Three different functions because the three formats have different special characters. Using the wrong one is almost as bad as using none.
Reading JSON you have received
If you go the other way and receive JSON, one more rule applies: never trust that the structure is as you expect. A field can be missing, have the wrong type, or contain something entirely different from what the sender promised. Check before you use, and decide what should happen when something is wrong — before the error occurs, not after.
Related
- A web service with multiple routesA complete program that responds to several addresses, reads the query string and returns both HTML and plain text.
- Store and retrieve dataCreate a table, write rows and read them back out — with the built-in database functions, and without one query per row.
- Hash a password safelyArgon2id from the standard library — the right tool for passwords, without a single external dependency.