{"id":13412159,"url":"https://github.com/motrboat/hotcoal","last_synced_at":"2025-03-14T18:30:22.968Z","repository":{"id":186896919,"uuid":"675951262","full_name":"motrboat/hotcoal","owner":"motrboat","description":"Hotcoal - Secure your handcrafted SQL against injection","archived":false,"fork":false,"pushed_at":"2023-12-23T06:21:35.000Z","size":53,"stargazers_count":17,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-07-31T20:49:56.398Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/motrboat.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2023-08-08T05:21:20.000Z","updated_at":"2024-07-21T13:30:22.000Z","dependencies_parsed_at":"2023-12-23T07:26:21.840Z","dependency_job_id":"eb179420-e83f-4b42-b5db-aca6693811eb","html_url":"https://github.com/motrboat/hotcoal","commit_stats":null,"previous_names":["motrboat/hotcoal"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/motrboat%2Fhotcoal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/motrboat%2Fhotcoal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/motrboat%2Fhotcoal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/motrboat%2Fhotcoal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/motrboat","download_url":"https://codeload.github.com/motrboat/hotcoal/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243624988,"owners_count":20321208,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-30T20:01:21.636Z","updated_at":"2025-03-14T18:30:22.593Z","avatar_url":"https://github.com/motrboat.png","language":"Go","funding_links":[],"categories":["Database","数据库","Data Integration Frameworks"],"sub_categories":["SQL Query Builders","SQL查询生成器"],"readme":"# Hotcoal\n\n[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)\n[![Go Reference](https://pkg.go.dev/badge/github.com/motrboat/hotcoal.svg)](https://pkg.go.dev/github.com/motrboat/hotcoal)\n[![Go Report Card](https://goreportcard.com/badge/github.com/motrboat/hotcoal)](https://goreportcard.com/report/github.com/motrboat/hotcoal)\n![Coverage](https://img.shields.io/badge/Coverage-97.3%25-brightgreen)\n\n## Secure your handcrafted SQL against injection\n\nMany Golang apps do not use an ORM, and instead prefer to use raw SQL for their database operations.\n\nEven though writing raw SQL can be sometimes more flexible, efficient and clean than using an ORM, it can make you vulnerable to SQL injection.\n\nWhile most database technologies (PostreSQL, MySQL, Sqlite etc.) and Golang SQL libraries (`database/sql`, `sqlx` etc.) allow you to use prepared statements, which take parameters and sanitize them, there are some limitations:\n * you can use parameters for values, but not for column names or table names\n * for more complex stuff, a good ORM can generate SQL dynamically for you, and do it safely; but you aren't using an ORM, so you start handcrafting SQL ...\n\nThis is where Hotcoal comes in.\n\n## How it works\n\nHotcoal provides a **minimal** API which helps you guard your handcrafted SQL against SQL injection.\n\n1. Handcraft your SQL using Hotcoal instead of plain strings. This way all your parameters are validated against the allowlists you provide.\n\n2. Convert the result to a plain string and use it with your SQL library.\n\nYou can use Hotcoal with any SQL library you like.\n\nHotcoal does not replace prepared queries with parameters, but complements it.\n\n## Examples\n\nWe use this example table:\n\n```sql\nCREATE TABLE \"users\" (\n\t\"id\"\tINTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,\n\t\"first_name\"\tTEXT NOT NULL,\n\t\"middle_name\"\tTEXT NOT NULL,\n\t\"last_name\"\tTEXT NOT NULL,\n\t\"nickname\"\tTEXT,\n\t\"email\"\tTEXT NOT NULL\n);\n```\n\n### Validate column name\n\nWe use Hotcoal to validate the column name and create a SQL query string.\n\nString constants can be converted to a hotcoalString directly, without validation, using `Wrap`.\n\nBut string variables cannot be converted to a hotcoalString directly. This helps protect against SQL injection.\n\nInstead, you must validate the string variable against an allowlist, using `Allowlist` and `Validate`. \u003cbr\u003e\nIf you try to convert a string variable to a hotcoalString without validation,\nusing `Wrap`, it doesn't compile.\n\n\n```golang\nimport (\n  \"database/sql\"\n  \"github.com/motrboat/hotcoal\"  \n)\n\nfunc queryCount(db *sql.DB, columnName string, value string) *sql.Row {\n\n  // validating the columnName variable against an allowlist\n  // it returns a hotcoalString\n  validatedColumnName, err := hotcoal.\n    Allowlist(\"first_name\", \"middle_name\", \"last_name\").\n    Validate(columnName)\n\n  if err != nil {\n    return nil, err\n  }  \n\n  // handcraft SQL using hotcoalStrings\n  query := hotcoal.Wrap(\"SELECT COUNT(*) FROM users WHERE \") +\n    validatedColumnName +\n    hotcoal.Wrap(\" = ?;\")\n\n  // if you try to use a regular string variable, which has not been validated,\n  // it's vulnerable to SQL injection, so it doesn't compile:\n  //       query := hotcoal.Wrap(\"SELECT COUNT(*) FROM users WHERE \") +\n  //         columnName +\n  //         hotcoal.Wrap(\" = ?;\")\n\n  // convert the hotcoalString back to a regular string only when it is passed to the DB library\n  // use prepared query with parameter to sanitize the value\n  row := db.QueryRow(query.String(), value)\n\n  return row\n}\n```\n\n### Validate handcrafted SQL\n\nTo handcraft more complicated SQL, you can also use the hotcoal equivalents\nof `strings` functions `Join`, `Replace` and `ReplaceAll`.\nThese can be called with hotcoalStrings, or string constants.\nBut not string variables - those must be validated first.\n\n```golang\nimport (\n  \"database/sql\"\n  \"github.com/motrboat/hotcoal\"  \n)\n\ntype Filter struct {\n  ColumnName string\n  Value string\n}\n\nfunc queryCount(db *sql.DB, tableName string, filters []Filter) (*sql.Row, error) {\n  validatedTableName, err := hotcoal.\n    Allowlist(\"users\", \"customers\").\n    Validate(tableName)\n  if err != nil {\n    return nil, err\n  }\n\n  allowlist := hotcoal.Allowlist(\"first_name\", \"middle_name\", \"last_name\", \"nickname\")\n\n  sqlArr := hotcoal.Slice{}\n  values := []string{}\n\n  for _, filter := range filters {\n    validatedColumnName, err := allowlist.Validate(filter.ColumnName)\n    if err != nil {\n      return nil, err\n    }\n\n    sqlArr = append(sqlArr, validatedColumnName + hotcoal.Wrap(\" = ?\"))\n    values = append(values, filter.Value)\n  }\n\n  query := hotcoal.Wrap(\"SELECT COUNT(*) FROM {{TABLE}} WHERE {{FILTERS}};\").\n    ReplaceAll(\n      \"{{TABLE}}\",\n      validatedTableName,\n    ).\n    ReplaceAll(\n      \"{{FILTERS}}\",\n      hotcoal.Join(sqlArr, \" OR \"),\n    )\n\n  row := db.QueryRow(query.String(), values...)\n\n  return row\n}\n```\n\n### String builder\n\nHotcoal also offers a version of `strings.Builder` using `hotcoalStrings`. It minimizes memory copying and is more efficient.\n\n```golang\nimport (\n  \"database/sql\"\n  \"github.com/motrboat/hotcoal\"  \n)\n\ntype Filter struct {\n  ColumnName string\n  Value string\n}\n\nfunc queryCount(db *sql.DB, filters []Filter) *sql.Row {\n  allowlist := hotcoal.Allowlist(\"first_name\", \"middle_name\", \"last_name\", \"nickname\")\n\n  builder := hotcoal.Builder{}\n  values := []string{}\n\n  builder.Write(\"SELECT COUNT(*) FROM users WHERE \")\n\n  for i, filter := range filters {\n    if i != 0 {\n      builder.Write(\" OR \")\n    }\n\n    validatedColumnName, err := allowlist.Validate(filter.ColumnName)\n    if err != nil {\n      return nil, err\n    }\n\n    builder.Write(validatedColumnName + hotcoal.Wrap(\" = ?\"))\n    values = append(values, filter.Value)\n  }\n\n  builder.Write(\";\")\n\n  row := db.QueryRow(builder.String(), values...)\n\n  return row\n}\n```\n\n## Documentation\n\n- [type hotcoalString](\u003c#type-hotcoalstring\u003e)\n  - [func Wrap\\(s hotcoalString\\) hotcoalString](\u003c#func-wrap\u003e)\n  - [func W\\(s hotcoalString\\) hotcoalString](\u003c#func-w\u003e)\n  - [func Itoa\\(i int\\) hotcoalString](\u003c#func-itoa\u003e)\n  - [func \\(s hotcoalString\\) String\\(\\) string](\u003c#func-hotcoalstring-string\u003e)\n- [type Slice](\u003c#type-slice\u003e)\n- [type allowlistT](\u003c#type-allowlistt\u003e)\n  - [func Allowlist\\(firstAllowlistItem hotcoalString, otherAllowlistItems ...hotcoalString\\) allowlistT](\u003c#func-allowlist\u003e)\n  - [func \\(a allowlistT\\) Validate\\(value string\\) \\(hotcoalString, error\\)](\u003c#func-allowlistt-validate\u003e)\n  - [func \\(a allowlistT\\) V\\(value string\\) \\(hotcoalString, error\\)](\u003c#func-allowlistt-v\u003e)\n  - [func \\(a allowlistT\\) MustValidate\\(value string\\) hotcoalString](\u003c#func-allowlistt-mustvalidate\u003e)\n  - [func \\(a allowlistT\\) MV\\(value string\\) hotcoalString](\u003c#func-allowlistt-mv\u003e)\n- [func Join\\(elems \\[\\]hotcoalString, sep hotcoalString\\) hotcoalString](\u003c#func-join\u003e)\n- [func \\(s hotcoalString\\) Replace\\(old, new hotcoalString, n int\\) hotcoalString](\u003c#func-hotcoalstring-replace\u003e)\n- [func \\(s hotcoalString\\) ReplaceAll\\(old, new hotcoalString\\) hotcoalString](\u003c#func-hotcoalstring-replaceall\u003e)\n- [type Builder](\u003c#type-builder\u003e)\n  - [func \\(b \\*Builder\\) Cap\\(\\) int](\u003c#func-builder-cap\u003e)\n  - [func \\(b \\*Builder\\) Grow\\(n int\\)](\u003c#func-builder-grow\u003e)\n  - [func \\(b \\*Builder\\) Len\\(\\) int](\u003c#func-builder-len\u003e)\n  - [func \\(b \\*Builder\\) Reset\\(\\)](\u003c#func-builder-reset\u003e)\n  - [func \\(b \\*Builder\\) Write\\(s hotcoalString\\) \\*Builder](\u003c#func-builder-write\u003e)\n  - [func \\(b \\*Builder\\) HotcoalString\\(\\) hotcoalString](\u003c#func-builder-hotcoalstring\u003e)\n  - [func \\(b \\*Builder\\) String\\(\\) string](\u003c#func-builder-string\u003e)\n\n\n### type [hotcoalString](\u003chttps://github.com/motrboat/hotcoal/blob/main/hotcoal.go#L6\u003e)\n\nhotcoalString is an abstract data type, which is used for handcrafting SQL, protecting against SQL injection\n\n```go\ntype hotcoalString string\n```\n\n\n#### func [Wrap](\u003chttps://github.com/motrboat/hotcoal/blob/main/hotcoal.go#L25\u003e)\n\n```go\nfunc Wrap(s hotcoalString) hotcoalString\n```\n\nThe Wrap function converts an untyped string constant to a hotcoalString. You can only use it with an untyped string constant, not with a string variable. For the latter, please use an Allowlist to validate the variable and guard against SQL injection.\n\n\n#### func [W](\u003chttps://github.com/motrboat/hotcoal/blob/main/hotcoal.go#L30\u003e)\n\n```go\nfunc W(s hotcoalString) hotcoalString\n```\n\nThe W function is a shorthand for Wrap\n\n\n#### func [Itoa](\u003chttps://github.com/motrboat/hotcoal/blob/main/strconv.go#L6\u003e)\n\n```go\nfunc Itoa(i int) hotcoalString\n```\n\nThe Itoa function converts an int to a hotcoalString.\n\n\n#### func \\(hotcoalString\\) [String](\u003chttps://github.com/motrboat/hotcoal/blob/main/hotcoal.go#L17\u003e)\n\n```go\nfunc (s hotcoalString) String() string\n```\n\nThe String method converts a hotcoalString to a plain string. Please do all your SQL handcrafting using hotcoalStrings, and convert the result to a plain string only when you pass it to the SQL library.\n\n\n### type [Slice](\u003chttps://github.com/motrboat/hotcoal/blob/main/hotcoal.go#L11\u003e)\n\nSlice is an alias for a slice of hotcoalStrings. Since hotcoalString is not exported, we export this alias, which allows you to create slices.\n\n```go\ntype Slice = []hotcoalString\n```\n\n\n### type [allowlistT](\u003chttps://github.com/motrboat/hotcoal/blob/main/allowlist.go#L7-L9\u003e)\n\nallowlistT holds an allowlist of items, which is used to validate string variables such as column names or table names, guarding against SQL injection\n\n```go\ntype allowlistT struct {\n    items map[hotcoalString]unitT\n}\n```\n\n\n#### func [Allowlist](\u003chttps://github.com/motrboat/hotcoal/blob/main/allowlist.go#L17\u003e)\n\n```go\nfunc Allowlist(firstAllowlistItem hotcoalString, otherAllowlistItems ...hotcoalString) allowlistT\n```\n\nAllowlist creates an allowlistT, which is used to validate validate string variables such as column names or table names, guarding against SQL injection\n\n\n#### func \\(allowlistT\\) [Validate](\u003chttps://github.com/motrboat/hotcoal/blob/main/allowlist.go#L33\u003e)\n\n```go\nfunc (a allowlistT) Validate(value string) (hotcoalString, error)\n```\n\nThe Validate method validates a string variable against the allowlist and returns a hotcoalString. If the value is not in the allowlist, it returns an error.\n\n\n#### func \\(allowlistT\\) [V](\u003chttps://github.com/motrboat/hotcoal/blob/main/allowlist.go#L42\u003e)\n\n```go\nfunc (a allowlistT) V(value string) (hotcoalString, error)\n```\n\nThe V method is an shorthand for Validate\n\n\n#### func \\(allowlistT\\) [MustValidate](\u003chttps://github.com/motrboat/hotcoal/blob/main/allowlist.go#L48\u003e)\n\n```go\nfunc (a allowlistT) MustValidate(value string) hotcoalString\n```\n\nThe MustValidate method validates a string variable against the allowlist and returns a hotcoalString. If the value is not in the allowlist, it panics.\n\n\n#### func \\(allowlistT\\) [MV](\u003chttps://github.com/motrboat/hotcoal/blob/main/allowlist.go#L58\u003e)\n\n```go\nfunc (a allowlistT) MV(value string) hotcoalString\n```\n\nThe MV method is an shorthand for MustValidate\n\n\n### func [Join](\u003chttps://github.com/motrboat/hotcoal/blob/main/strings.go#L9\u003e)\n\n```go\nfunc Join(elems []hotcoalString, sep hotcoalString) hotcoalString\n```\n\nJoin concatenates the elements of its first argument to create a single hotcoalString. The separator hotcoalString sep is placed between elements in the resulting hotcoalString.\n\nUnder the hood, it uses strings.Join https://pkg.go.dev/strings#Join\n\n\n### func \\(hotcoalString\\) [Replace](\u003chttps://github.com/motrboat/hotcoal/blob/main/strings.go#L68\u003e)\n\n```go\nfunc (s hotcoalString) Replace(old, new hotcoalString, n int) hotcoalString\n```\n\nReplace returns a copy of the hotcoalString s with the first n non\\-overlapping instances of old replaced by new. If old is empty, it matches at the beginning of the hotcoalString and after each UTF\\-8 sequence, yielding up to k\\+1 replacements for a k\\-rune hotcoalString. If n \\\u003c 0, there is no limit on the number of replacements.\n\nYou can chain method calls.\n\nUnder the hood, it uses strings.Replace https://pkg.go.dev/strings#Replace\n\n### func \\(hotcoalString\\) [ReplaceAll](\u003chttps://github.com/motrboat/hotcoal/blob/main/strings.go#L88\u003e)\n\n```go\nfunc (s hotcoalString) ReplaceAll(old, new hotcoalString) hotcoalString\n```\n\nReplaceAll returns a copy of the string s with all non\\-overlapping instances of old replaced by new. If old is empty, it matches at the beginning of the string and after each UTF\\-8 sequence, yielding up to k\\+1 replacements for a k\\-rune string.\n\nYou can chain method calls.\n\nUnder the hood, it uses strings.ReplaceAll https://pkg.go.dev/strings#ReplaceAll\n\n\n### type [Builder](\u003chttps://github.com/motrboat/hotcoal/blob/main/builder.go#L13-L15\u003e)\n\nA Builder is used to efficiently build a hotcoalString using the Write method. It minimizes memory copying. The zero value is ready to use. Do not copy a non\\-zero Builder.\n\nUnder the hood, it uses strings.Builder https://pkg.go.dev/strings#Builder\n\n```go\ntype Builder struct {\n    stringBuilder strings.Builder\n}\n```\n\n\n#### func \\(\\*Builder\\) [Cap](\u003chttps://github.com/motrboat/hotcoal/blob/main/builder.go#L20\u003e)\n\n```go\nfunc (b *Builder) Cap() int\n```\n\nCap returns the capacity of the builder's underlying byte slice. It is the total space allocated for the hotcoalString being built and includes any bytes already written.\n\n\n#### func \\(\\*Builder\\) [Grow](\u003chttps://github.com/motrboat/hotcoal/blob/main/builder.go#L27\u003e)\n\n```go\nfunc (b *Builder) Grow(n int)\n```\n\nGrow grows b's capacity, if necessary, to guarantee space for another n bytes. After Grow\\(n\\), at least n bytes can be written to b without another allocation. If n is negative, Grow panics.\n\n\n#### func \\(\\*Builder\\) [Len](\u003chttps://github.com/motrboat/hotcoal/blob/main/builder.go#L32\u003e)\n\n```go\nfunc (b *Builder) Len() int\n```\n\nLen returns the number of accumulated bytes; b.Len\\(\\) == len\\(b.String\\(\\)\\).\n\n\n#### func \\(\\*Builder\\) [Reset](\u003chttps://github.com/motrboat/hotcoal/blob/main/builder.go#L37\u003e)\n\n```go\nfunc (b *Builder) Reset()\n```\n\nReset resets the Builder to be empty.\n\n\n#### func \\(\\*Builder\\) [Write](\u003chttps://github.com/motrboat/hotcoal/blob/main/builder.go#L42\u003e)\n\n```go\nfunc (b *Builder) Write(s hotcoalString) *Builder\n```\n\nWrite appends the contents of s to b's buffer. It returns b, you can chain method calls.\n\n\n#### func \\(\\*Builder\\) [HotcoalString](\u003chttps://github.com/motrboat/hotcoal/blob/main/builder.go#L54\u003e)\n\n```go\nfunc (b *Builder) HotcoalString() hotcoalString\n```\n\nString returns the accumulated string as a hotcoalString.\n\n\n#### func \\(\\*Builder\\) [String](\u003chttps://github.com/motrboat/hotcoal/blob/main/builder.go#L59\u003e)\n\n```go\nfunc (b *Builder) String() string\n```\n\nString returns the accumulated string as a plain string.\n\n## Disclaimer\n\nHotcoal comes without any warranty.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmotrboat%2Fhotcoal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmotrboat%2Fhotcoal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmotrboat%2Fhotcoal/lists"}